Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,16 @@
# --- Chutes ---
#CHUTES_API_KEY_1="YOUR_CHUTES_API_KEY"

# --- Player2 (https://player2.game) ---
# Don't paste this by hand: run the credential tool (option "Add API Key" -> Player2)
# and use the assisted login instead. It detects a locally running Player2 app,
# or opens a browser to approve access, and saves the resulting key here for you.
#PLAYER2_API_KEY_1="obtained via the assisted login wizard, not pasted manually"
# Advanced: override the Game Client ID used for Player2 login (self-hosters
# running their own registered Player2 game). Most people should leave this
# unset - the assisted login wizard uses a project-provided default.
#PLAYER2_CLIENT_ID="your-player2-game-client-id"

Comment thread
coderabbitai[bot] marked this conversation as resolved.
# ------------------------------------------------------------------------------
# | [OAUTH] Provider OAuth 2.0 Credentials |
# ------------------------------------------------------------------------------
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,7 @@ python -m rotator_library.credential_tool
| Type | Providers | How to Add |
|------|-----------|------------|
| **API Keys** | Gemini, OpenAI, Anthropic, OpenRouter, Groq, Mistral, NVIDIA, Cohere, Chutes | Enter key in TUI or add to `.env` |
| **Assisted Login** | Player2 | Interactive wizard in the credential tool: auto-detects a local Player2 app, or opens a browser to approve access |
| **OAuth** | Gemini CLI | Interactive browser login via credential tool |

### The `.env` File
Expand Down
124 changes: 124 additions & 0 deletions src/rotator_library/credential_tool.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
import re
import time
from pathlib import Path
from typing import Optional
from dotenv import set_key, get_key

# NOTE: Heavy imports (provider_factory, PROVIDER_PLUGINS) are deferred
Expand Down Expand Up @@ -1150,6 +1151,120 @@ def _get_providers_by_category(providers: dict) -> dict:
by_category[category].append((name, config))
return by_category

async def _setup_player2_credential(api_key_var: str):
"""
Assisted login wizard for Player2 (https://player2.game).

Unlike a normal API key, a Player2 "p2Key" is meant to be obtained
through a login step rather than copy-pasted from a dashboard. This
wizard tries, in order:

1. Detecting a Player2 desktop app already running locally (instant,
no browser needed) - useful for the game-mod use case where players
already have the app open.
2. The OAuth 2.0 Device Authorization Flow (a URL + short code the user
opens on any device) - works headless, over SSH, in Docker, etc.

A manual "paste an existing p2Key" option is also offered, for people
who already have one.

Both flows need a Player2 "Game Client ID" (`PLAYER2_CLIENT_ID`),
registered once by the proxy operator via the Player2 Developer
Dashboard at https://player2.game. It's stored in .env like any other
setting and reused for all subsequent logins.
"""
from .providers.utilities.player2_auth import (
DEFAULT_CLIENT_ID,
Player2AuthError,
get_p2_key,
)

env_file = _get_env_file()

# The Game Client ID ships with the code (see player2_auth.DEFAULT_CLIENT_ID
# for details on why it's currently provisional) - end users never create
# or enter their own. An env var override is supported for forks/self-hosters
# who register their own Player2 game for separate usage attribution.
client_id = (
os.environ.get("PLAYER2_CLIENT_ID")
or (get_key(str(env_file), "PLAYER2_CLIENT_ID") if env_file.is_file() else None)
or DEFAULT_CLIENT_ID
)

console.print()
console.print("[bold]How would you like to sign in?[/bold]")
console.print(" 1. Auto (detect the Player2 app, or open a browser to approve)")
console.print(" 2. Paste an existing p2Key manually")
choice = Prompt.ask("Choice", choices=["1", "2"], default="1")

p2_key: Optional[str] = None

if choice == "2":
p2_key = Prompt.ask("[bold]Enter your p2Key[/bold]", password=True).strip() or None
else:
def on_verification(uri: str, user_code: str) -> None:
console.print()
console.print(
Panel(
Text.from_markup(
f"Open this URL to approve access:\n[bold cyan]{uri}[/bold cyan]\n\n"
f"Code: [bold]{user_code}[/bold]\n\n"
"[dim]Waiting for approval...[/dim]"
),
style="blue",
title="Player2 Login",
expand=False,
)
)
try:
import webbrowser

webbrowser.open(uri)
except Exception:
pass

console.print("\n[dim]Checking for a locally running Player2 app...[/dim]")
try:
p2_key = await get_p2_key(client_id, on_verification=on_verification)
except Player2AuthError as e:
console.print(Panel(str(e), style="bold red", title="Login Failed"))
return
except Exception as e:
console.print(
Panel(f"Unexpected error during Player2 login: {e}", style="bold red", title="Error")
)
return

if not p2_key:
console.print("[dim]No key obtained, cancelling.[/dim]")
return

# Reuse the same numbered-key convention as every other provider
# (PLAYER2_API_KEY_1, PLAYER2_API_KEY_2, ...).
key_index = 1
while True:
key_name = f"{api_key_var}_{key_index}"
if env_file.is_file():
with open(env_file, "r") as f:
if not any(line.startswith(f"{key_name}=") for line in f):
break
else:
break
key_index += 1

key_name = f"{api_key_var}_{key_index}"
set_key(str(env_file), key_name, p2_key)

masked = f"{p2_key[:4]}...{p2_key[-4:]}" if len(p2_key) > 8 else "****"
console.print(
Panel(
Text.from_markup(f"Saved [yellow]{key_name}[/yellow] = {masked}"),
style="bold green",
title="Success",
expand=False,
)
)


async def setup_api_key():
"""
Expand Down Expand Up @@ -1429,6 +1544,15 @@ async def setup_api_key():

saved_vars = []

# Player2 gets an assisted login wizard instead of a plain paste
# prompt: a p2Key is normally obtained from the local desktop app or
# an approval step, not copy-pasted from a dashboard.
if provider_key == "player2":
await _setup_player2_credential(api_key_var)
console.print("\n[dim]Press Enter to continue...[/dim]")
input()
return

# Prompt for API key (if provider has one)
if api_key_var:
api_key = Prompt.ask(
Expand Down
Loading