From e4eeb6dc7fbbcb0c4fc64694af03f98c5e789bef Mon Sep 17 00:00:00 2001 From: Paijo <14921983+oyi77@users.noreply.github.com> Date: Mon, 1 Jun 2026 17:44:52 +0700 Subject: [PATCH] refactor: Make SessionPool modular & provider-agnostic (#2978) Integrated into release/v3.8.8. Applied review fixes: typed DefaultExecutor.execute(input: ExecuteInput), guarded result?.response?.status, removed the unused cloakbrowser dependency. Session-pool modular refactor + 36 tests green. Thanks @oyi77! --- docs/poc/FREE_PROVIDERS_POC_REPORT.md | 157 ---------- docs/poc/OPENCODE_FREE_SESSION_POOL_POC.md | 95 ------ open-sse/executors/base.ts | 6 +- open-sse/executors/default.ts | 43 ++- open-sse/executors/pollinations.ts | 1 - open-sse/services/sessionPool/session.ts | 2 +- tests/poc/session-pool-opencode-poc.ts | 319 --------------------- 7 files changed, 46 insertions(+), 577 deletions(-) delete mode 100644 docs/poc/FREE_PROVIDERS_POC_REPORT.md delete mode 100644 docs/poc/OPENCODE_FREE_SESSION_POOL_POC.md delete mode 100644 tests/poc/session-pool-opencode-poc.ts diff --git a/docs/poc/FREE_PROVIDERS_POC_REPORT.md b/docs/poc/FREE_PROVIDERS_POC_REPORT.md deleted file mode 100644 index fc3fc220bd..0000000000 --- a/docs/poc/FREE_PROVIDERS_POC_REPORT.md +++ /dev/null @@ -1,157 +0,0 @@ -# PoC Report: Per-Provider Unlimited Access for Free AI Providers - -**Date**: 2026-05-31 -**Status**: Phase 1 Complete - ---- - -## Executive Summary - -Tested 30+ free AI providers to find which ones offer truly unlimited access without API keys. Found **4 providers** that work without any authentication and have no effective rate limits. - ---- - -## Test Results - -### ✅ TRULY UNLIMITED (No Auth, No Rate Limit) - -| Provider | Model | Success Rate | Notes | -|----------|-------|--------------|-------| -| **OpenCode Free** | `nemotron-3-super-free` | 20/20 (100%) | No custom `rateLimit` in model config | -| **Pollinations** | `openai` | 17/20 (85%) | 502 errors (upstream), no 429s | -| **UncloseAI** | `adamo1139/Hermes-3-Llama-3.1-8B-FP8-Dynamic` | 10/10 (100%) | Any string as API key | -| **LLM7.io** | `gpt-4o-mini` | 10/10 (100%) | 1 req/s, 10/min, 60/hr limit | - -### ⚠️ WORKING BUT RATE LIMITED - -| Provider | Model | Success Rate | Notes | -|----------|-------|--------------|-------| -| **OpenCode Free** | `deepseek-v4-flash-free` | 0/20 (0%) | Per-model daily limit (~10-20 req) | -| **OpenCode Free** | `big-pickle` | 0/20 (0%) | Per-model daily limit | -| **OpenCode Free** | `mimo-v2.5-free` | 0/20 (0%) | Per-model daily limit | -| **t3.chat** | any | 0/10 (0%) | 429 - needs cookies + session ID | - -### ❌ NEED API KEYS (401 Unauthorized) - -| Provider | Status | -|----------|--------| -| FreeModel.dev | Needs real API key | -| FreeAIAPIKey | Needs real API key | -| Nous Research | Needs real API key | -| BluesMinds | Needs real API key | -| AIML API | Needs real API key | -| PublicAI | Needs real API key | -| Inference.net | Needs real API key | -| Bytez | Needs real API key | -| Featherless AI | Needs real API key | -| Chutes | Needs real API key | -| Jina AI | Needs real API key | -| Arcee AI | Needs real API key | - -### ❌ NOT WORKING - -| Provider | Status | Error | -|----------|--------|-------| -| DuckDuckGo Web | VQD token not returned | API may have changed | -| HuggingChat | 302 redirect | Needs cookies | -| Meta AI | 400 Bad Request | Needs cookies | -| Qwen Web | 504 Gateway Timeout | Endpoint down | -| Phind | 403 Forbidden | Needs cookies | -| Novita | 404 Not Found | Endpoint changed | -| Voyage AI | 404 Not Found | No chat endpoint | -| FriendliAI | 404 Not Found | No chat endpoint | -| AI21 | 404 Not Found | No chat endpoint | -| Poolside | curl 000 | Connection failed | -| InclusionAI | curl 000 | Connection failed | -| Liquid | curl 000 | Connection failed | -| Nomic | curl 000 | Connection failed | -| Krutrim | curl 000 | Connection failed | -| MonsterAPI | curl 000 | Connection failed | -| Lepton | curl 000 | Connection failed | -| Predibase | curl 000 | Connection failed | -| GLHF | Timeout | Endpoint unresponsive | - ---- - -## Key Findings - -### 1. OpenCode Free Rate Limiting Mechanism - -From `anomalyco/opencode` repo source code: - -``` -Rate limit key: {stage}:ratelimit:ip:{ip}:{date}{model-prefix} -``` - -- **IP-based**: Rate limits tied to client IP (Cloudflare sets `x-real-ip`) -- **Per-model buckets**: When `rateLimit` is defined in model config, each model has its own limit -- **Shared bucket**: When `rateLimit` is undefined, uses shared bucket with higher limit -- **Daily reset**: Resets at midnight UTC - -**Why nemotron is unlimited**: No custom `rateLimit` in OpenCode's model config → uses shared bucket with high limit. - -**Why deepseek is limited**: Has custom `rateLimit` → per-model bucket with low limit (~10-20 req/day). - -### 2. IP Spoofing Doesn't Work - -Tested `x-real-ip` header spoofing with: -- IPv4 private ranges (10.x, 192.168.x, 172.16.x) -- IPv6 addresses (fd00::x) -- Different User-Agents - -**Result**: Cloudflare overrides `x-real-ip` with real client IP. All spoofed IPs ignored. - -### 3. Pollinations Has No Rate Limiting - -50 requests tested: -- No 429 (rate limit) responses -- Only 502 (upstream errors) - transient failures, not rate limits -- Session pool with fingerprint rotation already integrated - -### 4. UncloseAI Works With Any Key - -- Accepts any non-empty string as API key -- No rate limiting detected (10/10 success) -- Models: `adamo1139/Hermes-3-Llama-3.1-8B-FP8-Dynamic`, `qwen3.6:27b`, `gemma4:31b` - -### 5. LLM7.io Has Token-Based Rate Limiting - -- Use `"unused"` as API key -- Rate limits: 1 req/s, 10/min, 60/hr -- Works perfectly with 2s delay between requests - ---- - -## Recommendations - -### Immediate Wins (No Implementation Needed) - -1. **Use `nemotron-3-super-free`** via OpenCode Free - already unlimited -2. **Use Pollinations** - already has session pool, no rate limits -3. **Use UncloseAI** - any key works, no rate limits -4. **Use LLM7.io** - works with 2s delay between requests - -### For Cookie-Based Providers - -These need account generators + cookie rotation: -- HuggingChat -- Meta AI -- t3.chat -- Qwen Web -- Phind - -### For IP-Based Rate Limits - -These need proxy rotation: -- OpenCode Free (other models with per-model limits) -- DuckDuckGo Web (if VQD issue resolved) - ---- - -## Files - -- **Plan**: `.omo/plans/unlimited-free-providers.md` -- **PoC Script**: `tests/poc/session-pool-opencode-poc.ts` -- **Session Pool**: `open-sse/services/sessionPool/` -- **OpenCode Executor**: `open-sse/executors/opencode.ts` -- **Pollinations Executor**: `open-sse/executors/pollinations.ts` diff --git a/docs/poc/OPENCODE_FREE_SESSION_POOL_POC.md b/docs/poc/OPENCODE_FREE_SESSION_POOL_POC.md deleted file mode 100644 index 22007885a0..0000000000 --- a/docs/poc/OPENCODE_FREE_SESSION_POOL_POC.md +++ /dev/null @@ -1,95 +0,0 @@ -# PoC: SessionPool × OpenCode Free — IP Spoofing for Unlimited Access - -## Summary - -This PoC tests whether the SessionPool's fingerprint rotation combined with IP spoofing can bypass rate limiting on the OpenCode Free public endpoint (`https://opencode.ai/zen/v1/chat/completions`). - -**Key Finding**: IP spoofing via `x-real-ip` header **does NOT work** — Cloudflare overrides it with the real client IP. However, `nemotron-3-super-free` has **no effective rate limit** (50+ requests tested, all succeeded). Other free models (deepseek, big-pickle, mimo) have per-model daily limits. - -## How OpenCode Free Works (from source code) - -The OpenCode CLI uses `apiKey: "public"` as a dummy key when no API key is configured. The backend reads the IP from `x-real-ip` header (set by Cloudflare) and applies per-model daily rate limits. - -### Rate Limit Architecture - -From `packages/console/app/src/routes/zen/util/ipRateLimiter.ts`: -- IP extracted from `x-real-ip` header (Cloudflare sets this) -- Daily limit per IP, resets at midnight UTC -- `retry-after` header = seconds until midnight UTC -- Rate limit key: `{stage}:ratelimit:ip:{ip}:{date}` -- Per-model limits via `modelId.substring(0, 2)` prefix when `rateLimit` is defined - -From `packages/opencode/src/provider/provider.ts`: -- When no API key: filters to free models only (`cost.input === 0`) -- Uses `apiKey: "public"` as dummy bearer token -- `zenApiKey === "public"` → treated as `undefined` (anonymous) - -## Test Results - -### nemotron-3-super-free — 100% success rate - -``` -Total requests: 50+ -✅ Success (200): 50+ (100%) -⏳ Rate limited: 0 -💀 Server errors: 0 -``` - -### Other free models — rate limited - -``` -deepseek-v4-flash-free: 429 (per-model daily limit exhausted) -big-pickle: 429 (per-model daily limit exhausted) -mimo-v2.5-free: 429 (per-model daily limit exhausted) -``` - -### IP Spoofing Test - -``` -Spoofed IPs tested: 10.0.0.1-255, 192.168.x.x, fd00::x -Result: ALL requests use real IP (Cloudflare overrides x-real-ip) -``` - -## Why nemotron Works - -The `nemotron-3-super-free` model either: -1. Has no custom `rateLimit` defined (uses shared bucket with high limit) -2. Has a very high per-model daily limit (50+) -3. Is provisioned through NVIDIA's free tier with generous limits - -## Solution: Use nemotron-3-super-free as Primary - -The "unlimited" approach is simple: -1. Use `nemotron-3-super-free` as the primary model (no rate limit) -2. Fall back to other free models when needed -3. Use the session pool for request management and retry logic - -## Implementation Plan - -1. Update OmniRoute's OpenCode executor to prefer `nemotron-3-super-free` -2. Add fallback chain: nemotron → deepseek → big-pickle → mimo -3. Use session pool for request queuing and automatic model rotation -4. Track per-model rate limits and switch models when one is exhausted - -## Reproduction - -```bash -# Test nemotron (works) -curl -X POST https://opencode.ai/zen/v1/chat/completions \ - -H "Content-Type: application/json" \ - -H "Authorization: Bearer public" \ - -d '{"model":"nemotron-3-super-free","messages":[{"role":"user","content":"Say ok"}],"stream":false}' - -# Test deepseek (rate limited after ~10 requests) -curl -X POST https://opencode.ai/zen/v1/chat/completions \ - -H "Content-Type: application/json" \ - -H "Authorization: Bearer public" \ - -d '{"model":"deepseek-v4-flash-free","messages":[{"role":"user","content":"Say ok"}],"stream":false}' -``` - -## Files - -- **PoC script**: `tests/poc/session-pool-opencode-poc.ts` -- **Session pool**: `open-sse/services/sessionPool/` -- **OpenCode executor**: `open-sse/executors/opencode.ts` -- **Source reference**: `anomalyco/opencode` repo, `packages/console/app/src/routes/zen/util/ipRateLimiter.ts` diff --git a/open-sse/executors/base.ts b/open-sse/executors/base.ts index 82bd16c66f..6324f7bb94 100644 --- a/open-sse/executors/base.ts +++ b/open-sse/executors/base.ts @@ -3,6 +3,8 @@ import { applyFingerprint, isCliCompatEnabled } from "../config/cliFingerprints. import { supportsClaudeMaxEffort, supportsXHighEffort } from "../config/providerModels.ts"; import type { PoolConfig } from "../services/sessionPool/types.ts"; import type { Session } from "../services/sessionPool/session.ts"; +import { SessionPool } from "../services/sessionPool/sessionPool.ts"; +import { PoolRegistry } from "../services/sessionPool/poolRegistry.ts"; import { getRotatingApiKey, getValidApiKey, @@ -336,11 +338,9 @@ export class BaseExecutor { return this.provider; } - protected getPool(): import("../services/sessionPool/sessionPool.ts").SessionPool | null { + protected getPool(): SessionPool | null { if (!this.poolConfig) return null; if (!this._pool) { - const { SessionPool } = require("../services/sessionPool/sessionPool.ts"); - const { PoolRegistry } = require("../services/sessionPool/poolRegistry.ts"); const pool = new SessionPool(this.provider, this.poolConfig); pool.warmUp(this.poolConfig.minSessions).catch(() => {}); PoolRegistry.register(this.provider, pool); diff --git a/open-sse/executors/default.ts b/open-sse/executors/default.ts index 3dd2ac39af..8595e82d39 100644 --- a/open-sse/executors/default.ts +++ b/open-sse/executors/default.ts @@ -1,4 +1,4 @@ -import { BaseExecutor, setUserAgentHeader } from "./base.ts"; +import { BaseExecutor, setUserAgentHeader, type ExecuteInput } from "./base.ts"; import { PROVIDERS, OAUTH_ENDPOINTS } from "../config/constants.ts"; import { getAccessToken } from "../services/tokenRefresh.ts"; import { @@ -552,6 +552,47 @@ export class DefaultExecutor extends BaseExecutor { } return super.needsRefresh(credentials); } + + async execute(input: ExecuteInput) { + const pool = this.getPool(); + if (!pool) return super.execute(input); + + const session = pool.acquire(); + if (session) { + input.upstreamExtraHeaders = { + ...session.buildHeaders(), + ...input.upstreamExtraHeaders, + }; + } + + let result; + try { + result = await super.execute(input); + } catch (err) { + if (session) { + pool.reportCooldown(session); + session.release(); + } + throw err; + } + + if (session) { + try { + const status = result?.response?.status; + if (status === 429) { + pool.reportCooldown(session); + } else if (status >= 500) { + pool.reportDead(session); + } else { + pool.reportSuccess(session); + } + } finally { + session.release(); + } + } + + return result; + } } export default DefaultExecutor; diff --git a/open-sse/executors/pollinations.ts b/open-sse/executors/pollinations.ts index eae763e458..3f5330d93a 100644 --- a/open-sse/executors/pollinations.ts +++ b/open-sse/executors/pollinations.ts @@ -80,7 +80,6 @@ export class PollinationsExecutor extends BaseExecutor { pool.reportDead(session); } else { pool.reportSuccess(session); - pool.totalRequests++; } } finally { session.release(); diff --git a/open-sse/services/sessionPool/session.ts b/open-sse/services/sessionPool/session.ts index b596e5057b..26e465a9a2 100644 --- a/open-sse/services/sessionPool/session.ts +++ b/open-sse/services/sessionPool/session.ts @@ -34,7 +34,7 @@ export class Session { private readonly cooldownMax: number, private readonly cooldownJitter: number, ) { - this.id = `sess-${fingerprint.id}-${Date.now().toString(36)}`; + this.id = `sess-${fingerprint.id}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`; this.fingerprint = fingerprint; this.createdAt = Date.now(); } diff --git a/tests/poc/session-pool-opencode-poc.ts b/tests/poc/session-pool-opencode-poc.ts deleted file mode 100644 index 8be003834e..0000000000 --- a/tests/poc/session-pool-opencode-poc.ts +++ /dev/null @@ -1,319 +0,0 @@ -#!/usr/bin/env node --import tsx/esm -/** - * PoC: SessionPool + FingerprintRotator × OpenCode Free - * ======================================================= - * - * Demonstrates that rotating browser fingerprints across sessions - * defeats burst rate-limiting on the OpenCode Free public endpoint. - * - * Run: - * node --import tsx/esm tests/poc/session-pool-opencode-poc.ts - * - * Output: TAP-style results + summary table. - * - * For a "no-pool" baseline, set BASELINE=1 - * BASELINE=1 node --import tsx/esm tests/poc/session-pool-opencode-poc.ts - */ - -import { setTimeout as sleep } from "node:timers/promises"; -import { SessionPool } from "../../open-sse/services/sessionPool/sessionPool.ts"; -import { FingerprintRotator } from "../../open-sse/services/sessionPool/fingerprintRotator.ts"; -import type { PoolConfig } from "../../open-sse/services/sessionPool/types.ts"; -import { DEFAULT_POOL_CONFIG } from "../../open-sse/services/sessionPool/types.ts"; - -// ─── Config ───────────────────────────────────────────────────────────────── - -import { readFileSync } from "node:fs"; -import { homedir } from "node:os"; -import path from "node:path"; - -const OPENCODE_CHAT_URL = "https://opencode.ai/zen/v1/chat/completions"; -const USE_AUTH_KEY = process.env.USE_AUTH_KEY || ""; -const USE_POOL = !process.env.BASELINE; - -function getOpencodeApiKey(): string { - if (USE_AUTH_KEY) return USE_AUTH_KEY; - try { - const authPath = path.join(homedir(), ".local", "share", "opencode", "auth.json"); - const raw = readFileSync(authPath, "utf8"); - const auth = JSON.parse(raw); - return auth?.opencode?.key ?? ""; - } catch { - return ""; - } -} - -const API_KEY = getOpencodeApiKey(); - -const FAST_CONFIG: PoolConfig = { - ...DEFAULT_POOL_CONFIG, - cooldownBase: 100, - cooldownMax: 1000, - cooldownJitter: 50, - minSessions: 3, - maxSessions: 10, -}; - -const TEST_PROMPT = "Say 'ok' and nothing else."; -const REQUEST_COUNT = 30; -const CONCURRENCY = 3; -const ROUNDS = 3; // how many times to repeat the batch - -// ─── Helpers ───────────────────────────────────────────────────────────────── - -function now(): string { - return new Date().toISOString().slice(11, 19); -} - -function divider(title: string): void { - console.log(`\n# ${"━".repeat(60)}`); - console.log(`# ${title}`); - console.log(`# ${"━".repeat(60)}`); -} - -interface AttemptResult { - ok: boolean; - status: number | string; - latencyMs: number; - fingerprintId?: string; - model?: string; -} - -// ─── Pool-based approach ───────────────────────────────────────────────────── - -async function runWithPool(): Promise { - const results: AttemptResult[] = []; - const pool = new SessionPool("opencode-poc", FAST_CONFIG); - await pool.warmUp(FAST_CONFIG.minSessions); - console.log(`# Pool warmed: ${pool.totalCount} sessions, ${pool.availableCount} available`); - - for (let round = 1; round <= ROUNDS; round++) { - console.log(`\n# === Round ${round}/${ROUNDS} (${REQUEST_COUNT} sequential requests) ===`); - - for (let i = 0; i < REQUEST_COUNT; i++) { - const session = pool.acquire(); - if (!session) { - console.log(`# [${now()}] req ${i + 1}: NO SESSION AVAILABLE (all on cooldown)`); - results.push({ ok: false, status: "no-session", latencyMs: 0 }); - await sleep(200); // backoff before retry - continue; - } - - const fpId = session.fingerprint.id; - const poolHeaders: Record = { - "Content-Type": "application/json", - Accept: "text/event-stream", - }; - if (API_KEY) { - poolHeaders["Authorization"] = `Bearer ${API_KEY}`; - } - const headers = session.buildHeaders(poolHeaders); - - const body = JSON.stringify({ - model: "deepseek-v4-flash-free", - messages: [{ role: "user", content: TEST_PROMPT }], - stream: false, - }); - - const start = performance.now(); - try { - const res = await fetch(OPENCODE_CHAT_URL, { - method: "POST", - headers, - body, - signal: AbortSignal.timeout(15_000), - }); - - const latency = Math.round(performance.now() - start); - const status = res.status; - - if (status === 429) { - pool.reportCooldown(session); - console.log(`# [${now()}] req ${i + 1} [${fpId}] 429 RATE LIMITED (${latency}ms) → cooldown`); - results.push({ ok: false, status, latencyMs: latency, fingerprintId: fpId }); - } else if (status >= 500) { - pool.reportDead(session); - console.log(`# [${now()}] req ${i + 1} [${fpId}] ${status} SERVER ERROR (${latency}ms) → dead`); - results.push({ ok: false, status, latencyMs: latency, fingerprintId: fpId }); - } else if (status === 200) { - const text = await res.text(); - let model = "?"; - try { - const json = JSON.parse(text); - model = json.model ?? json?.choices?.[0]?.message?.content?.slice(0, 20) ?? "?"; - } catch { /* ignore parse errors */ } - pool.reportSuccess(session); - console.log(`# [${now()}] req ${i + 1} [${fpId}] 200 OK (${latency}ms) model=${model}`); - results.push({ ok: true, status: 200, latencyMs: latency, fingerprintId: fpId, model }); - } else { - console.log(`# [${now()}] req ${i + 1} [${fpId}] ${status} UNEXPECTED (${latency}ms)`); - results.push({ ok: false, status, latencyMs: latency, fingerprintId: fpId }); - } - } catch (err: any) { - const latency = Math.round(performance.now() - start); - const errMsg = err?.name === "TimeoutError" ? "TIMEOUT" : err?.message?.slice(0, 60) ?? String(err); - console.log(`# [${now()}] req ${i + 1} [${fpId}] ERROR: ${errMsg} (${latency}ms)`); - pool.reportDead(session); - results.push({ ok: false, status: `error:${errMsg}`, latencyMs: latency, fingerprintId: fpId }); - } finally { - session.release(); - } - - // Small jitter between requests to avoid overwhelming - await sleep(50 + Math.random() * 100); - } - - // Print pool stats after each round - const stats = pool.getStats(); - console.log(`\n# Pool after round ${round}:`); - console.log(`# sessions: ${stats.sessions.active} active, ${stats.sessions.cooldown} cooldown, ${stats.sessions.dead} dead`); - console.log(`# requests: ${stats.requests.total} total, ${stats.requests.success} success`); - console.log(`# rate-429: ${stats.requests.rate429}, errors: ${stats.requests.otherErrors}`); - - // Let pool recover between rounds - if (round < ROUNDS) { - console.log(`# Waiting 2s for cooldown recovery before next round...`); - await sleep(2000); - } - } - - await pool.shutdown(); - return results; -} - -// ─── Baseline (no pool) ────────────────────────────────────────────────────── - -async function runBaseline(): Promise { - const results: AttemptResult[] = []; - const rotator = new FingerprintRotator(); - - for (let round = 1; round <= ROUNDS; round++) { - console.log(`\n# === Round ${round}/${ROUNDS} (${REQUEST_COUNT} requests, same fingerprint) ===`); - - // Use the same fingerprint for the entire round (no session rotation = baseline) - const fp = rotator.next(); - const headers: Record = { - "Content-Type": "application/json", - Accept: "text/event-stream", - "User-Agent": fp.userAgent, - "Accept-Language": fp.acceptLanguage ?? "en-US,en;q=0.9", - }; - if (API_KEY) { - headers["Authorization"] = `Bearer ${API_KEY}`; - } - if (fp.secChUa) { - headers["Sec-CH-UA"] = fp.secChUa; - headers["Sec-CH-UA-Mobile"] = fp.secChUaMobile ?? "?0"; - headers["Sec-CH-UA-Platform"] = fp.secChUaPlatform ?? '"Windows"'; - } - - console.log(`# Using fingerprint: ${fp.id}`); - - for (let i = 0; i < REQUEST_COUNT * 1.5; i++) { - const body = JSON.stringify({ - model: "deepseek-v4-flash-free", - messages: [{ role: "user", content: TEST_PROMPT }], - stream: false, - }); - - const start = performance.now(); - try { - const res = await fetch(OPENCODE_CHAT_URL, { - method: "POST", - headers, - body, - signal: AbortSignal.timeout(15_000), - }); - const latency = Math.round(performance.now() - start); - const status = res.status; - - if (status === 200) { - console.log(`# [${now()}] req ${i + 1} 200 OK (${latency}ms)`); - results.push({ ok: true, status, latencyMs: latency, fingerprintId: fp.id }); - } else if (status === 429) { - console.log(`# [${now()}] req ${i + 1} 429 RATE LIMITED (${latency}ms) — backing off 3s`); - results.push({ ok: false, status, latencyMs: latency, fingerprintId: fp.id }); - await sleep(3000); - } else { - console.log(`# [${now()}] req ${i + 1} ${status} (${latency}ms)`); - results.push({ ok: false, status, latencyMs: latency, fingerprintId: fp.id }); - } - } catch (err: any) { - const latency = Math.round(performance.now() - start); - const errMsg = err?.name === "TimeoutError" ? "TIMEOUT" : err?.message?.slice(0, 60) ?? String(err); - console.log(`# [${now()}] req ${i + 1} ERROR: ${errMsg} (${latency}ms)`); - results.push({ ok: false, status: `error:${errMsg}`, latencyMs: latency, fingerprintId: fp.id }); - } - - await sleep(100 + Math.random() * 200); - } - } - - return results; -} - -// ─── Report ────────────────────────────────────────────────────────────────── - -function printReport(results: AttemptResult[], label: string): void { - const total = results.length; - const ok = results.filter((r) => r.ok).length; - const rateLimited = results.filter((r) => r.status === 429).length; - const serverErrors = results.filter((r) => typeof r.status === "number" && r.status >= 500).length; - const timeouts = results.filter((r) => String(r.status).includes("TIMEOUT")).length; - const noSessions = results.filter((r) => r.status === "no-session").length; - const other = total - ok - rateLimited - serverErrors - timeouts - noSessions; - - const latencies = results.filter((r) => r.latencyMs > 0).map((r) => r.latencyMs); - const avgLatency = latencies.length > 0 ? Math.round(latencies.reduce((a, b) => a + b, 0) / latencies.length) : 0; - const sorted = [...latencies].sort((a, b) => a - b); - const p50 = sorted[Math.floor(sorted.length * 0.5)] ?? 0; - const p95 = sorted[Math.floor(sorted.length * 0.95)] ?? 0; - - const uniqueFps = new Set(results.filter((r) => r.fingerprintId).map((r) => r.fingerprintId)); - - console.log(`\n${"━".repeat(70)}`); - console.log(` 📊 REPORT: ${label}`); - console.log(`${"━".repeat(70)}`); - console.log(` Total requests: ${total}`); - console.log(` ✅ Success (200): ${ok} (${(ok / total * 100).toFixed(1)}%)`); - console.log(` ⏳ Rate limited: ${rateLimited}`); - console.log(` 💀 Server errors: ${serverErrors}`); - console.log(` ⌛ Timeouts: ${timeouts}`); - console.log(` ❌ No sessions: ${noSessions}`); - console.log(` ? Other: ${other}`); - console.log(` ───────────────────────────`); - console.log(` Avg latency: ${avgLatency}ms`); - console.log(` p50 latency: ${p50}ms`); - console.log(` p95 latency: ${p95}ms`); - console.log(` Unique fingerprints: ${uniqueFps.size}`); - console.log(`${"━".repeat(70)}`); -} - -// ─── Main ──────────────────────────────────────────────────────────────────── - -async function main(): Promise { - const keyMasked = API_KEY ? `${API_KEY.slice(0, 8)}...${API_KEY.slice(-4)}` : "NONE"; - console.log(`# ═══════════════════════════════════════════════════════════════`); - console.log(`# PoC: SessionPool × OpenCode Free`); - console.log(`# Mode: ${USE_POOL ? "WITH session pool (fingerprint rotation)" : "BASELINE (single fingerprint)"}`); - console.log(`# Endpoint: ${OPENCODE_CHAT_URL}`); - console.log(`# Auth: ${API_KEY ? `YES (key: ${keyMasked})` : "NO (anonymous)"}`); - console.log(`# Requests per round: ${REQUEST_COUNT} × ${ROUNDS} rounds`); - console.log(`# ═══════════════════════════════════════════════════════════════`); - - let results: AttemptResult[]; - if (USE_POOL) { - results = await runWithPool(); - printReport(results, "SessionPool + FingerprintRotator"); - } else { - results = await runBaseline(); - printReport(results, "Baseline (single fingerprint, no pool)"); - } - - console.log(`\n# PoC complete.`); -} - -main().catch((err) => { - console.error("PoC failed:", err); - process.exit(1); -});