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
41 changes: 37 additions & 4 deletions backend/llm_providers.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,24 @@
from openai import OpenAI
from typing import Dict, Any, Optional


def _sanitize_env_value(value: Optional[str]) -> Optional[str]:
"""
Clean up env-provided strings that may include surrounding quotes or whitespace.

Some shells/export flows set values like OPENROUTER_API_KEY="sk-or-...".
The OpenAI SDK forwards the raw string, so we strip wrapping quotes here
to avoid 401s that look like "No cookie auth credentials found".
"""
if value is None:
return None
cleaned = value.strip()
if len(cleaned) >= 2 and (
(cleaned[0] == '"' and cleaned[-1] == '"') or (cleaned[0] == "'" and cleaned[-1] == "'")
):
cleaned = cleaned[1:-1].strip()
return cleaned

class LLMProviderInterface:
"""
A common interface for LLM calls.
Expand Down Expand Up @@ -51,8 +69,9 @@ def extract_api_kwargs(config: Dict[str, Any]) -> Dict[str, Any]:

class OpenRouterProvider(LLMProviderInterface):
def __init__(self, api_key: str, config: Dict[str, Any]):
base_url = os.getenv("OPENROUTER_BASE_URL", "https://openrouter.ai/api/v1")
self.client = OpenAI(api_key=api_key, base_url=base_url)
raw_base_url = os.getenv("OPENROUTER_BASE_URL")
base_url = _sanitize_env_value(raw_base_url) or "https://openrouter.ai/api/v1"
self.client = OpenAI(api_key=_sanitize_env_value(api_key) or api_key, base_url=base_url)
self.model_name = config['model_name']
self.api_type = config.get('api_type', 'completions')
self.api_kwargs = self.extract_api_kwargs(config)
Expand All @@ -74,6 +93,19 @@ def get_response(self, prompt: str) -> Dict[str, Any]:
if self.extra_headers:
request_kwargs['extra_headers'] = self.extra_headers

# Add middle-out transform for automatic context compression (OpenRouter feature)
# OpenRouter expects transforms inside extra_body, not as a top-level kwarg.
extra_body = request_kwargs.pop("extra_body", {}) or {}
transforms = request_kwargs.pop("transforms", None)
if not transforms:
transforms = ["middle-out"]
if isinstance(extra_body, dict):
extra_body = dict(extra_body)
extra_body["transforms"] = transforms
else:
extra_body = {"transforms": transforms}
request_kwargs["extra_body"] = extra_body

if self.api_type == 'responses':
response = self.client.responses.create(
model=self.model_name,
Expand Down Expand Up @@ -184,7 +216,8 @@ def create_llm_provider(player_config: Dict[str, Any]) -> LLMProviderInterface:
Factory function for creating an LLM provider instance.
All models now route through OpenRouter.
"""
if not os.getenv("OPENROUTER_API_KEY"):
openrouter_api_key = _sanitize_env_value(os.getenv("OPENROUTER_API_KEY"))
if not openrouter_api_key:
raise ValueError("OPENROUTER_API_KEY is not set in the environment variables.")

return OpenRouterProvider(api_key=os.getenv("OPENROUTER_API_KEY"), config=player_config)
return OpenRouterProvider(api_key=openrouter_api_key, config=player_config)
1 change: 1 addition & 0 deletions backend/players/llm_player.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ def _construct_prompt(self, game_state: GameState) -> str:
f"The board size is {game_state.width}x{game_state.height}. Normal X,Y coordinates are used. "
f"Coordinates range from (0,0) at bottom left to ({game_state.width-1},{game_state.height-1}) at top right. "
"All snake coordinate lists are ordered head-to-tail: the first tuple is the head, each subsequent tuple connects to the previous one, and the last tuple is the tail.\n"
"IMPORTANT: Do not perform web searches or access external information. Use only the game state provided.\n"
f"{turn_line}\n\n"
f"Apples at: {apples_str}\n\n"
f"Scores so far:\n"
Expand Down