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!
This commit is contained in:
Paijo
2026-06-01 17:44:52 +07:00
committed by GitHub
parent 4de674e0fc
commit e4eeb6dc7f
7 changed files with 46 additions and 577 deletions

View File

@@ -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`

View File

@@ -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`

View File

@@ -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);

View File

@@ -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;

View File

@@ -80,7 +80,6 @@ export class PollinationsExecutor extends BaseExecutor {
pool.reportDead(session);
} else {
pool.reportSuccess(session);
pool.totalRequests++;
}
} finally {
session.release();

View File

@@ -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();
}

View File

@@ -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<AttemptResult[]> {
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<string, string> = {
"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<AttemptResult[]> {
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<string, string> = {
"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<void> {
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);
});