Unified async Python access to major AI web apps using browser-session cookies instead of official API keys.
llm-cookie-bridge is a lightweight Python library that gives you a single async interface for talking to popular AI web apps through the same authenticated browser sessions you already use.
It currently supports:
- Google Gemini web
- ChatGPT / OpenAI web
- Claude web
- Perplexity web
This project is designed for engineers who need a consistent chat + streaming abstraction across multiple providers, but need to authenticate with cookies or session-derived web tokens rather than first-party API credentials.
- Why this exists
- What this project is — and is not
- Features
- Installation
- Quick start
- Provider setup
- Streaming
- Refresh and session recovery
- API overview
- Provider-specific chat options
- Error model
- Security model
- Testing
- Development
- Research references
- Publishing
- License
The major AI web apps all expose different internal request formats, auth bootstraps, and streaming behaviors. If you want to build tooling around the web products rather than the official APIs, you usually end up re-implementing the same plumbing repeatedly:
- turning browser cookies into authenticated requests
- discovering ephemeral web tokens
- normalizing SSE or frame-based streaming formats
- recovering from expired sessions
- keeping provider-specific parsing logic out of your application code
LLMCookieBridge packages that work into one minimal library with a stable Python interface.
- a unified async client for multiple AI web products
- a cookie/session bridge for authenticated browser-backed access
- a good fit for experimentation, internal tools, migration utilities, and research workflows
- intentionally small, with only
httpxas a runtime dependency
- an official SDK for any provider
- a compatibility promise for undocumented endpoints
- a production SLA surface
- a way to bypass provider terms, rate limits, billing, or account restrictions
Warning
This package targets reverse-engineered web endpoints that may change at any time and without notice. Treat it as an unstable bridge around consumer web products, not as a long-term guaranteed integration surface.
- Unified provider interface via
LLMCookieBridge.create(...) - Async-first API built on
httpx.AsyncClient - Streaming support with normalized chunk objects
- Best-effort session refresh for each provider
- Custom refresh callbacks for external cookie renewal flows
- Minimal dependency footprint
- Pinned-host security defaults for authenticated requests
- Mock-transport-friendly design for unit testing
- Conversation continuity where providers support it
pip install llm-cookie-bridge- Python 3.11+
- An authenticated session for the target provider
import asyncio
import os
from llm_cookie_bridge import LLMCookieBridge
async def main() -> None:
bridge = LLMCookieBridge.create(
"chatgpt",
cookies={
"__Secure-next-auth.session-token": os.environ["CHATGPT_SESSION_TOKEN"],
},
)
async with bridge:
response = await bridge.chat("Say hello in one sentence.")
print(response.text)
async for chunk in bridge.stream("Write a short poem about HTTP."):
print(chunk.delta, end="", flush=True)
asyncio.run(main())chat() returns a ChatResponse:
@dataclass(slots=True)
class ChatResponse:
provider: str
text: str
conversation_id: str | None
message_id: str | None
raw_events: list[Any]
metadata: dict[str, Any]stream() yields ChatChunk objects:
@dataclass(slots=True)
class ChatChunk:
provider: str
text: str
delta: str
done: bool = False
conversation_id: str | None = None
message_id: str | None = None
raw: Any = None
metadata: dict[str, Any] = field(default_factory=dict)Each provider has slightly different authentication material and bootstrap behavior.
Expected cookies typically include:
__Secure-1PSID__Secure-1PSIDTS
import os
from llm_cookie_bridge import LLMCookieBridge
bridge = LLMCookieBridge.create(
"gemini",
cookies={
"__Secure-1PSID": os.environ["GEMINI_1PSID"],
"__Secure-1PSIDTS": os.environ["GEMINI_1PSIDTS"],
},
)Under the hood, Gemini bootstrap extracts web app state such as:
SNlM0eaccess token- build label (
bl) - session id (
f.sid) - language metadata
Expected cookie:
__Secure-next-auth.session-token
import os
from llm_cookie_bridge import LLMCookieBridge
bridge = LLMCookieBridge.create(
"chatgpt",
cookies={
"__Secure-next-auth.session-token": os.environ["CHATGPT_SESSION_TOKEN"],
},
)If you already have a valid web bearer token, you can also initialize directly with access_token:
bridge = LLMCookieBridge.create(
"chatgpt",
access_token="...",
)Claude commonly works best with a full cookie header string, for example one containing sessionKey=....
import os
from llm_cookie_bridge import LLMCookieBridge
bridge = LLMCookieBridge.create(
"claude",
cookie_header=os.environ["CLAUDE_COOKIE_HEADER"],
)During refresh, the bridge discovers the active Claude organization UUID automatically.
Expected cookie:
__Secure-next-auth.session-token
import os
from llm_cookie_bridge import LLMCookieBridge
bridge = LLMCookieBridge.create(
"perplexity",
cookies={
"__Secure-next-auth.session-token": os.environ["PERPLEXITY_SESSION_TOKEN"],
},
)Perplexity performs a lightweight session-prime step before chat requests.
All providers are exposed through the same streaming interface:
async with bridge:
async for chunk in bridge.stream("Summarize this repo in three bullets."):
if chunk.done:
break
print(chunk.delta, end="", flush=True)chunk.textis the latest full accumulated text for that messagechunk.deltais the newly added suffix when it can be derived- the final yielded chunk has
done=True conversation_idandmessage_idare preserved when the provider exposes them
Every provider implements a best-effort refresh() flow:
- Gemini: reloads app bootstrap state and extracts required web tokens
- ChatGPT: fetches a bearer token from the web session endpoint
- Claude: discovers the active organization UUID
- Perplexity: re-primes the next-auth session endpoint
You can also provide a custom callback to renew cookies when a session expires.
async def refresh_cookies(provider_name: str):
assert provider_name == "claude"
return {"sessionKey": "new-cookie-value"}
bridge = LLMCookieBridge.create(
"claude",
cookie_header="sessionKey=stale-cookie",
refresh_callback=refresh_cookies,
)For more control, return CookieRefreshResult:
from llm_cookie_bridge import CookieRefreshResult
async def refresh_session(provider_name: str) -> CookieRefreshResult:
return CookieRefreshResult(
cookies={"__Secure-next-auth.session-token": "fresh-cookie"},
metadata={"source": "external-secret-store"},
)The callback may return:
- a plain
dict[str, str]of cookies - a
CookieRefreshResult None
bridge = LLMCookieBridge.create(
provider,
cookies=None,
cookie_header=None,
headers=None,
timeout=30.0,
transport=None,
refresh_callback=None,
allow_custom_base_url=False,
follow_redirects=False,
**provider_kwargs,
)async with bridge:
...
await bridge.aclose()await bridge.refresh(force=False)
await bridge.chat(message, **kwargs)
async for chunk in bridge.stream(message, **kwargs):
...| Argument | Description |
|---|---|
cookies |
Cookie map passed into the underlying httpx.AsyncClient |
cookie_header |
Raw cookie header string, parsed and merged into cookies |
headers |
Additional request headers, sanitized against reserved auth-sensitive names |
timeout |
Request timeout in seconds |
transport |
Custom httpx transport, useful for tests and mocks |
refresh_callback |
Callback invoked on auth recovery paths |
allow_custom_base_url |
Required for cross-host authenticated overrides |
follow_redirects |
Disabled by default for safer authenticated behavior |
These are forwarded through bridge.chat(..., **kwargs) and bridge.stream(..., **kwargs).
| Option | Meaning |
|---|---|
conversation_id |
Continue an existing conversation |
parent_id |
Explicit parent message id |
model |
ChatGPT web model selector, defaults to "auto" |
disable_history |
Sets history_and_training_disabled |
Notes:
- The bridge remembers the last conversation/message id during the session.
- Follow-up turns reuse the last assistant message id automatically.
| Option | Meaning |
|---|---|
conversation_id |
Continue an existing Claude conversation |
model |
Claude model id |
timezone |
Defaults to "UTC" |
attachments |
Attachment payload passthrough |
files |
File payload passthrough |
Notes:
- If no conversation exists, the bridge creates one automatically.
- Claude rate limit responses may raise
RateLimitError.
Gemini currently exposes a minimal user-facing surface and derives the request envelope internally from the prompt and bootstrapped app state.
| Option | Meaning |
|---|---|
mode |
"auto" or explicit non-auto mode |
incognito |
Whether to send an incognito flag |
language |
Defaults to "en-US" |
last_backend_uuid |
Continue from a previous backend state |
model_preference |
Perplexity model preference override |
sources |
Defaults to ["web"] |
version |
Web request version string |
attachments |
Attachment payload passthrough |
The public exception types are:
BridgeError— base exceptionAuthenticationError— auth bootstrap or refresh failedProviderResponseError— provider returned a non-2xx HTTP responseParseError— response could not be parsedRateLimitError— provider indicated usage or rate limiting
Example:
from llm_cookie_bridge import AuthenticationError, LLMCookieBridge, RateLimitError
try:
async with LLMCookieBridge.create("claude", cookie_header="sessionKey=...") as bridge:
await bridge.chat("Hello")
except AuthenticationError:
print("Session expired or cookies are invalid.")
except RateLimitError:
print("Provider rate limit reached.")Because this library handles authenticated browser sessions, the defaults are intentionally strict.
- provider hosts are pinned by default
- cross-host base URL overrides are rejected unless
allow_custom_base_url=True - redirects are disabled by default
- user-supplied
authorization,cookie,host,origin, andrefererheaders are rejected - cookie maps are merged explicitly rather than blindly proxying a raw client config
- Do not feed untrusted input into
cookies,cookie_header,headers, orbase_url - treat each bridge instance as single-session and single-tenant
- do not reuse one authenticated bridge across multiple end users
- expect provider-side auth, anti-abuse, or request-shape changes at any time
The test suite uses mocked HTTP transports to lock down request shapes, auth flows, parser behavior, and security defaults.
Run tests locally:
pytestWhat is currently covered:
- ChatGPT session bootstrap and conversation streaming
- follow-up turn parent message reuse
- Claude organization discovery and chat creation
- Gemini bootstrap token extraction and frame parsing
- Perplexity SSE answer extraction
- refresh callback behavior
- security defaults around base URLs and reserved headers
Clone the repo, create an environment, install dev dependencies, and run tests:
python -m venv .venv
source .venv/bin/activate
pip install -e .[dev]
pytest- Single abstraction, provider-specific internals
- Async by default
- Minimal dependencies
- Testable transports and parsers
- Secure defaults for authenticated traffic
src/llm_cookie_bridge/
├── client.py # public LLMCookieBridge entrypoint
├── exceptions.py # public exception types
├── providers/ # provider implementations
├── sse.py # SSE parsing helpers
├── types.py # ChatChunk / ChatResponse / CookieRefreshResult
└── utils.py # shared parsing and request utilities
tests/
└── ... # provider and security regression tests
These projects informed request shapes and auth bootstrap understanding, but are not dependencies:
- Gemini:
HanaokaYuzu/Gemini-API - ChatGPT:
acheong08/ChatGPT,lanqian528/chat2api - Claude:
Xerxes-2/clewdr,st1vms/unofficial-claude-api,KoushikNavuluri/Claude-API - Perplexity:
helallao/perplexity-ai,henrique-coder/perplexity-webui-scraper,nathanrchn/perplexityai
This repository is configured for PyPI Trusted Publishing from GitHub Actions via:
.github/workflows/publish.yml
To publish a release:
- Configure the repository as a Trusted Publisher on PyPI
- Create a GitHub Release
- Let the publish workflow build and upload the new version automatically
MIT