From 96daaba605a51b8afe3275df0cf8f00f031abfae Mon Sep 17 00:00:00 2001 From: Cameron Pfiffer Date: Wed, 3 Dec 2025 21:36:33 -0800 Subject: [PATCH] feat: use OAuth token for Letta pipe authentication --- backend/functions/letta_agents.py | 62 +++++++++++++++++++++++++++---- 1 file changed, 54 insertions(+), 8 deletions(-) diff --git a/backend/functions/letta_agents.py b/backend/functions/letta_agents.py index e0f87d1ca..e6abe261f 100644 --- a/backend/functions/letta_agents.py +++ b/backend/functions/letta_agents.py @@ -7,7 +7,7 @@ requirements: letta-client """ from pydantic import BaseModel, Field -from typing import Generator, Iterator, Union +from typing import Generator, Iterator, Union, Optional import logging log = logging.getLogger(__name__) @@ -21,25 +21,38 @@ class Pipe: ) LETTA_API_KEY: str = Field( default="", - description="API key for Letta Cloud" + description="API key for listing agents (optional if using OAuth)" ) def __init__(self): self.valves = self.Valves() self.name = "" - def _get_client(self): + def _get_client(self, token: Optional[str] = None): """Get a Letta client instance.""" from letta_client import Letta kwargs = {"base_url": self.valves.LETTA_BASE_URL} - if self.valves.LETTA_API_KEY: + # Use provided token (OAuth) first, fall back to API key from valves + if token: + kwargs["api_key"] = token + elif self.valves.LETTA_API_KEY: kwargs["api_key"] = self.valves.LETTA_API_KEY return Letta(**kwargs) def pipes(self) -> list[dict]: """Return list of Letta agents as available models.""" + # If no API key is configured, return a placeholder entry + # The actual agent list will be fetched when the user sends a message + # using their OAuth token + if not self.valves.LETTA_API_KEY: + log.info("Letta pipe: no API key configured, returning placeholder") + return [{ + "id": "default", + "name": "Letta Agent (select agent in chat)" + }] + try: client = self._get_client() agents = [] @@ -57,9 +70,18 @@ class Pipe: except Exception as e: log.error(f"Error fetching Letta agents: {e}") - return [] - - def pipe(self, body: dict) -> Union[str, Generator, Iterator]: + # Return placeholder on error too + return [{ + "id": "default", + "name": "Letta Agent (select agent in chat)" + }] + + def pipe( + self, + body: dict, + __user__: Optional[dict] = None, + __oauth_token__: Optional[dict] = None, + ) -> Union[str, Generator, Iterator]: """ Process a chat completion request and forward to Letta agent. @@ -68,10 +90,18 @@ class Pipe: - model: The agent ID (format: "letta_pipe.agent-xxx") - messages: List of chat messages - stream: Whether to stream the response + __user__: User info dict from Open WebUI + __oauth_token__: OAuth token dict with access_token from Letta OAuth Returns: String response or generator for streaming """ + # Get OAuth access token if available + oauth_access_token = None + if __oauth_token__ and isinstance(__oauth_token__, dict): + oauth_access_token = __oauth_token__.get("access_token") + log.info(f"Letta pipe: using OAuth token for user {__user__.get('id', 'unknown') if __user__ else 'unknown'}") + # Extract agent ID from model name # Model format is "pipe_id.agent_id" - we need the agent_id part model = body.get("model", "") @@ -80,6 +110,22 @@ class Pipe: else: agent_id = model + # Handle placeholder "default" agent - list user's agents and use first one + if agent_id == "default": + if not oauth_access_token: + return "Error: Please log in with Letta to use this agent" + try: + client = self._get_client(token=oauth_access_token) + page = client.agents.list() + if page.items: + agent_id = page.items[0].id + log.info(f"Letta pipe: using first agent {agent_id}") + else: + return "Error: No agents found in your Letta account. Create an agent at app.letta.com first." + except Exception as e: + log.error(f"Error listing agents: {e}") + return f"Error listing agents: {str(e)}" + # Get messages - convert from OpenAI format to Letta format messages = body.get("messages", []) @@ -115,7 +161,7 @@ class Pipe: return "Error: No user message found" try: - client = self._get_client() + client = self._get_client(token=oauth_access_token) if body.get("stream", False): return self._stream_response(client, agent_id, [last_user_msg]) -- 2.51.2