Standalone Python package extracted from g4f's LMArena provider (with some modifications).
Goals:
- Async client library for
lmarena.ai - Optional FastAPI server with OpenAI-ish endpoints:
GET /v1/modelsPOST /v1/chat/completions
- Uses nodriver for auth + reCAPTCHA
- Uses aiohttp for HTTP
lmarena-client is a standalone Python 3.11+ package that:
- boots a real Chromium-based browser via nodriver to pass anti-bot + generate reCAPTCHA tokens,
- uses aiohttp for the actual API calls to
lmarena.ai, - exposes:
- a Python async client library, and
- an optional FastAPI server with OpenAI-ish endpoints:
GET /v1/modelsPOST /v1/chat/completions(supports streaming)
Key limitation:
- We ignore multi-turn roles/history from the request and only send the last user message (LMArena keeps history server-side per
evaluationSessionId).
From your repo folder:
pip install -e .pip install -e ".[server]"By default, the package runs headful (GUI browser) for better reliability with Turnstile/reCAPTCHA. You can override settings using env vars:
| Variable | Meaning |
|---|---|
LM_ARENA_BROWSER_EXECUTABLE_PATH |
Path to Chrome/Brave/Chromium executable |
LM_ARENA_BROWSER_USER_DATA_DIR |
User data dir (persistent profile) |
LM_ARENA_BROWSER_PROFILE_DIRECTORY |
Profile directory name (e.g. "Default") |
LM_ARENA_BROWSER_INCOGNITO |
1/true/yes to run incognito |
LM_ARENA_BROWSER_HEADLESS |
1/true/yes to run headless (--headless=new) |
LM_ARENA_HOST |
Server host (when running server) |
LM_ARENA_PORT |
Server port (when running server) |
Example (Windows PowerShell):
$env:LM_ARENA_BROWSER_EXECUTABLE_PATH="C:\Program Files\BraveSoftware\Brave-Browser\Application\brave.exe"
$env:LM_ARENA_BROWSER_USER_DATA_DIR="C:\Users\<you>\AppData\Local\BraveSoftware\Brave-Browser\User Data"
$env:LM_ARENA_BROWSER_PROFILE_DIRECTORY="Default"
$env:LM_ARENA_BROWSER_HEADLESS="0"python -m lmarena_clientlmarena-serverBy default it binds to:
127.0.0.1:1337
Override:
LM_ARENA_HOST=0.0.0.0 LM_ARENA_PORT=8000 lmarena-serverOn server startup it:
- boots the browser and performs the “bootstrap” flow (cookies + turnstile assist + grecaptcha readiness),
- loads live models and Next.js action IDs needed for image upload.
Returns a list of models (OpenAI-ish shape):
curl http://127.0.0.1:1337/v1/modelsMinimal request:
curl -X POST http://127.0.0.1:1337/v1/chat/completions \
-H "Content-Type: application/json" \
-d "{\"model\":\"gemini-3-pro\",\"messages\":[{\"role\":\"user\",\"content\":\"hello\"}]}"Response includes a vendor extension:
conversation.evaluationSessionId(this is the real resume key)
Example response:
{
"id": "chatcmpl-f0ea528f7853432d80e2ff2c9b718b9d",
"object": "chat.completion",
"created": 1768644722,
"model": "gemini-3-pro",
"choices": [
{
"index": 0,
"message": {
"role": "assistant",
"content": "Hello! How can I help you today?"
},
"finish_reason": "stop"
}
],
"conversation": {
"evaluationSessionId": "019bcb70-a714-7d9b-bb79-e2ae55270f67"
}
}To continue a chat, pass:
"conversation": { "evaluationSessionId": "..." }Example:
curl -X POST http://127.0.0.1:1337/v1/chat/completions \
-H "Content-Type: application/json" \
-d "{\"model\":\"gemini-3-pro\",\"conversation\":{\"evaluationSessionId\":\"<ID>\"},\"messages\":[{\"role\":\"user\",\"content\":\"continue\"}]}"This endpoint returns text/event-stream with OpenAI-style data: ... chunks and a final data: [DONE].
Example:
curl -N -X POST http://127.0.0.1:1337/v1/chat/completions \
-H "Content-Type: application/json" \
-d "{\"model\":\"gemini-3-pro\",\"stream\":true,\"messages\":[{\"role\":\"user\",\"content\":\"hello\"}]}"The server accepts OpenAI-style content parts in the last user message:
{
"model": "gemini-3-pro",
"messages": [{
"role": "user",
"content": [
{"type": "text", "text": "What is in this image?"},
{"type": "image_url", "image_url": {"url": "data:image/png;base64,...."}}
]
}]
}Notes:
- The image must be a valid
data:URI, or anhttp(s)URL. - Images are uploaded to LMArena using Next.js actions (
generateUploadUrl/getSignedUrl) and then included asexperimental_attachments.
import asyncio
from lmarena_client import Client
async def main():
client = Client()
await client.bootstrap()
models = await client.list_models()
print("Models:", models)
chat = await client.chats.create(model='gemini-3-pro')
# non-stream
r1 = await chat.send("1+1=", stream=False)
print("Reply:", r1.text)
print("Chat ID (evaluationSessionId):", r1.evaluation_session_id)
# resume (explicitly)
chat2 = await client.chats.resume(model='gemini-3-pro', chat_id=r1.evaluation_session_id)
r2 = await chat2.send("add 5 to the result", stream=False)
print("Reply2:", r2.text)
asyncio.run(main())import asyncio
from lmarena_client import Client
async def stream_example():
client = Client()
await client.bootstrap()
models = await client.list_models()
chat = await client.chats.create(model='gemini-3-pro')
stream = await chat.send("write a short article about global warming", stream=True)
async for delta in stream:
print(delta, end="", flush=True) #not delta.content, delta is str
# We can get the id after stream completes, this is the id to persist/use for resume
print("\nChat eval id (post):", chat.conversation.evaluation_session_id)
asyncio.run(stream_example())ChatSession.send(..., images=...) expects:
images=[(image_data, filename_or_none), ...]Where image_data can be:
- bytes
data:image/...;base64,...- path (
"C:/path/to/image.png") - file-like object
http(s)://...URL (fetched with aiohttp)
Example:
r = await chat.send(
"What's in this image?",
images=[("data:image/png;base64,....", "img.png")],
stream=False,
)
print(r.text)- Single-user / low-QPS design: nodriver operations are serialized behind a lock. This is intentional for simplicity.
- History handling: We do not send full message history to LMArena. LMArena maintains history server-side per
evaluationSessionId. - Reliability: headful mode is default because Turnstile/reCAPTCHA is often more reliable than headless. You can enable headless via
LM_ARENA_BROWSER_HEADLESS=1. - Persistence: the library does not store chats. If you want persistence, store the returned
evaluationSessionIdyourself and resume later.
An optional WebUI is included, built with Vite + React + TypeScript and served by the same FastAPI server under:
GET /ui(SPA entry)GET /ui/assets/*(static assets)
The WebUI is a single-page chat application that:
- Lists models from
GET /v1/models - Supports multiple conversations with per-chat model selection
- Streams responses (OpenAI-style SSE) or non-streaming responses
- Persists chats locally (IndexedDB) with a configurable max chat count (default 50)
- Handles conversation continuation via
conversation.evaluationSessionId - Supports image upload in the last user message (OpenAI-style
image_urlparts) - Renders markdown with syntax-highlighted code fences and “copy code” buttons
- Allows copying raw message text and the
evaluationSessionIdfor a chat - Provides a Settings drawer (theme, streaming on/off, max chats, export/import)
Frontend source lives in:
webui/(Vite project: React + TS + Tailwind)
Build output is written into the Python package:
lmarena_client/webui_dist/
These built assets are included as package data and served by FastAPI.
From the project root:
cd webui
npm install # or pnpm install / yarn install
npm run buildThis writes the production build into lmarena_client/webui_dist/.
After that, running the server as usual:
lmarena-serverwill serve the WebUI at:
http://127.0.0.1:1337/ui
For frontend development with hot reload, run:
cd webui
npm install # first time only
npm run devThis starts Vite on http://127.0.0.1:5173 and proxies API calls to the Python server at http://127.0.0.1:1337 for /v1/* endpoints.