mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-31 04:12:10 +03:00
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:
@@ -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`
|
||||
@@ -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`
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -80,7 +80,6 @@ export class PollinationsExecutor extends BaseExecutor {
|
||||
pool.reportDead(session);
|
||||
} else {
|
||||
pool.reportSuccess(session);
|
||||
pool.totalRequests++;
|
||||
}
|
||||
} finally {
|
||||
session.release();
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
Reference in New Issue
Block a user