Files
OmniRoute/docs/issues/plugin-browser-pool-proposal.md
oyi77 4919aa89a9 fix: remove duplicate getMod/modPromise in browserBackedChat stub
Two copies of the module proxy got committed — the typed BrowserPoolModule
version at lines 50-56 and a stale any-typed duplicate at lines 64-71.
Removed the duplicate, keeping the typed version.

Verification:
- 40/40 browser tests pass (both previously-failing suites now green)
- typecheck:core: 0 errors
- env kill switch (OMNIROUTE_BROWSER_POOL=off): verified
2026-07-23 21:56:32 +07:00

8.3 KiB

[Feature] Pluginization Phase 1: Extract Playwright/CloakBrowser Browser Pool

Labels: enhancement, plugin, architecture

Problem / Use Case

OmniRoute's Playwright/CloakBrowser dependency is a large, non-essential dependency (~1500+ LOC across 22 source files + ~35 test files) pulled into every installation regardless of whether the user needs browser-backed chat. Users who run OmniRoute purely as a proxy/router (the majority) pay for:

  • Disk space: ~200+ MB from Playwright browsers + Chromium binaries (installed via npx playwright install)
  • Build complexity: Turbopack must handle the cloakbrowser package
  • Bundle size: All browser-pool code is compiled into the main codebase
  • Surface area: 7 files with direct Playwright imports (4 dynamic, 3 static type imports)
  • CI/cache impact: Playwright installs in CI pipelines even when not needed

Currently, only environment variables (OMNIROUTE_BROWSER_POOL=off) gate runtime execution — the code still loads, imports get resolved, and Playwright must be installed.

Proposed Solution

Extract the Playwright/CloakBrowser browser pool into an optional package loaded via dynamic import() at runtime, following the existing pattern used for cloakbrowser (computed-string dynamic import to avoid resolution). The core retains thin interface stubs that gracefully degrade when the optional package is absent.

Architecture

open-sse/
  interfaces/
    browserPool.ts       ← NEW: BrowserPoolProvider interface + types
  services/
    browserPool.ts       ← BECOMES: thin stub, delegates to optional package
    browserBackedChat.ts ← BECOMES: thin stub, delegates to optional package
    grokClearance.ts     ← BECOMES: thin stub

packages/browser-pool/   ← NEW: optional package
  index.ts               ← exports BrowserPoolProvider implementation
  src/
    browserPool.ts       ← extracted from open-sse/services/browserPool.ts
    browserBackedChat.ts ← extracted from open-sse/services/browserBackedChat.ts
    grokClearance.ts     ← extracted from open-sse/services/grokClearance.ts
    claudeTurnstileSolver.ts ← extracted (tightly coupled, moved as-is)
    inAppLoginService.ts ← extracted (own Playwright instance, separate lifecycle)
  package.json
  tsconfig.json

Phase Breakdown

Phase 1 — Core pool extraction (this issue):

  1. Define BrowserPoolProvider interface in open-sse/interfaces/browserPool.ts
  2. Extract browserPool.ts (~502 LOC), browserBackedChat.ts (~270 LOC), grokClearance.ts (~84 LOC) into packages/browser-pool/
  3. Replace core files with thin stubs that try import('../../../packages/browser-pool') with graceful fallback
  4. Keep poolTools.ts importing the core stub (unchanged from consumer perspective)
  5. Make Playwright an optional dependency (not in root package.json)
  6. Typecheck core passes with and without the package installed
  7. All existing tests pass (with plugin installed)

Phase 2 — Turnstile solver extraction (future):

  • Extract claudeTurnstileSolver.ts (~212 LOC) — has static Playwright type imports, needs type interface
  • Move claudeWebAutoRefresh.ts (depends on turnstile solver)

Phase 3 — Standalone Playwright instances (future):

  • Extract inAppLoginService.ts (~257 LOC)
  • Refactor gemini-web.ts executor's own Playwright path (~553 LOC)

Interface Design (Phase 1)

// open-sse/interfaces/browserPool.ts
export interface BrowserPoolProvider {
  acquireBrowserContext(options?: BrowserPoolContextOptions): Promise<PooledContext>;
  releaseBrowserContext(ctx: PooledContext): Promise<void>;
  getBrowserPoolMetrics(): BrowserPoolMetrics;
  shutdownPool(): Promise<void>;
  isPoolEnabled(): boolean;
  openPage(url: string, ctx?: PooledContext): Promise<{ page: any }>;
  readPageResponseBody(page: any): Promise<string>;
  getBrowserPoolStatus(): BrowserPoolStatus;
}

Stub Pattern

// open-sse/services/browserPool.ts — thin stub
let _impl: BrowserPoolProvider | null = null;

async function getImpl(): Promise<BrowserPoolProvider> {
  if (!_impl) {
    try {
      const { createBrowserPoolProvider } = await import("../../packages/browser-pool");
      _impl = createBrowserPoolProvider();
    } catch {
      // Graceful fallback — disabled
      _impl = createNullBrowserPoolProvider();
    }
  }
  return _impl;
}

export async function acquireBrowserContext(...args) {
  return (await getImpl()).acquireBrowserContext(...args);
}

Alternatives Considered

  1. Existing hook-based PluginManager: Rejected. The current PluginManager operates via child-process IPC and request-pipeline hooks (onRequest, onResponse, onError). A browser pool is an in-process runtime service with composable lifecycle — not a request pipeline hook. Forcing it through IPC would add ~50ms+ per browser operation and break the existing synchronous pool pattern.

  2. Keep as-is, just lazy-load the import: Minimal improvement — the dependency tree still references Playwright types, requiring it to be available. Doesn't reduce bundle size or simplify CI.

  3. Replace Playwright with a protocol-level abstraction: Too ambitious and would change the behavior of the pool. Playwright's CDP capabilities (context isolation, cookies, screenshots) are fundamental to how the pool works.

  4. Monorepo workspace: Too heavy for this scope. A simple extracted package avoids workspace tooling changes.

Acceptance Criteria

  1. open-sse/interfaces/browserPool.ts exists and exports BrowserPoolProvider, PooledContext, BrowserPoolMetrics types
  2. open-sse/services/browserPool.ts becomes a thin stub with zero Playwright imports
  3. packages/browser-pool/ contains all extracted implementation (browserPool, browserBackedChat, grokClearance)
  4. Core typecheck (npm run typecheck:core) passes with 0 errors without the browser-pool package installed
  5. Core typecheck passes with the package installed
  6. All existing tests pass when the browser-pool package is installed
  7. poolTools.ts omniroute_browser_pool_status tool works end-to-end when the package is installed
  8. Graceful degradation: when the package is absent, getBrowserPoolStatus() returns { enabled: false } without crashing
  9. Playwright is moved from root dependencies to optional/peer in the extracted package
  10. Documentation updated in docs/reference/ENVIRONMENT.md

Expected Test Plan

  • Unit tests for the stub fallback path (simulate import failure, verify graceful degradation)
  • Unit tests moved to the extracted package
  • Verify tests/unit/browser-pool-optional-import.test.ts passes (still validates cloakbrowser isn't statically resolved)
  • Verify tests/unit/browserPool-proxy.test.ts passes
  • Verify tests/unit/browserBackedChat-matcher.test.ts passes
  • E2E: npm run typecheck:core without the package installed → 0 errors
  • E2E: npm run test:coverage (with package installed) → existing coverage gates pass

Additional Context

Current dependency graph (simplified):

open-sse/services/browserPool.ts (502 LOC, singleton Playwright/CloakBrowser pool)
  ├── open-sse/services/browserBackedChat.ts (270 LOC, browser-backed chat runner)
  │     ├── open-sse/executors/claude-web.ts (imports tryBackedChat)
  │     └── open-sse/executors/duckduckgo-web.ts (imports tryBackedChat)
  ├── open-sse/services/grokClearance.ts (84 LOC, CF clearance via browser)
  └── open-sse/mcp-server/tools/poolTools.ts (imports getBrowserPoolMetrics)

Standalone Playwright users (separate, future phases):
  ├── open-sse/services/claudeTurnstileSolver.ts (212 LOC, static Playwright type imports)
  ├── open-sse/services/inAppLoginService.ts (257 LOC, own browser lifecycle)
  └── open-sse/executors/gemini-web.ts (553 LOC, private Playwright path)

Kill switches: OMNIROUTE_BROWSER_POOL, WEB_COOKIE_USE_BROWSER (both env vars)

Total extracted in Phase 1: ~856 LOC, 3 files. Total deferred to Phase 2/3: ~1022 LOC, 4 files.

This is the first pluginization step. Future targets (separate issues): memory/compression plugin, additional provider support extraction.

  • PR #8219 (model catalog connection filter + cache TTL) — same baseline release/v3.8.49
  • docs/reference/ENVIRONMENT.md — browser pool env vars documentation
  • Plugin system docs at docs/PLUGINS.md — existing PluginManager (not used here, referenced for contrast)