### Install Dependencies and Run LLMcord (No Docker) Source: https://github.com/jakobdylanc/llmcord/blob/main/README.md Installs project dependencies using pip and then runs the LLMcord application. Ensure you have Python installed. ```bash python -m pip install -U -r requirements.txt python llmcord.py ``` -------------------------------- ### Clone llmcord Repository Source: https://github.com/jakobdylanc/llmcord/blob/main/README.md Clone the llmcord repository and navigate into the project directory to begin setup. ```bash git clone https://github.com/jakobdylanc/llmcord cd llmcord ``` -------------------------------- ### Run LLMcord with Docker Source: https://github.com/jakobdylanc/llmcord/blob/main/README.md Starts the LLMcord application using Docker Compose. This is a convenient way to manage the application's environment. ```bash docker compose up ``` -------------------------------- ### Running llmcord Without Docker Source: https://context7.com/jakobdylanc/llmcord/llms.txt Instructions for setting up and running the llmcord bot locally. Requires cloning the repository, editing the configuration file, installing dependencies, and running the main script. ```bash git clone https://github.com/jakobdylanc/llmcord cd llmcord cp config-example.yaml config.yaml # Edit config.yaml with your bot_token, api keys, and models pip install -U -r requirements.txt python llmcord.py ``` -------------------------------- ### Get or Create Message Node Source: https://context7.com/jakobdylanc/llmcord/llms.txt Ensures a message node exists and acquires a lock for safe access. Initializes node properties if it's a new node. ```python node = msg_nodes.setdefault(message.id, MsgNode()) async with node.lock: if node.text is None: node.role = "user" node.text = "<@123456789>: Hello!" node.images = [{"type": "image_url", "image_url": {"url": "data:image/png;base64,..."}}] ``` -------------------------------- ### Running llmcord With Docker Compose Source: https://context7.com/jakobdylanc/llmcord/llms.txt Instructions for deploying the llmcord bot using Docker Compose. This involves cloning the repository, configuring `config.yaml`, and running the `docker compose up` command. ```bash git clone https://github.com/jakobdylanc/llmcord cd llmcord cp config-example.yaml config.yaml # Edit config.yaml docker compose up ``` -------------------------------- ### Docker Compose Configuration for llmcord Source: https://context7.com/jakobdylanc/llmcord/llms.txt A sample `docker-compose.yaml` file for deploying the llmcord bot. It defines the service, build context, volume mounts for configuration, and restart policy. ```yaml services: llmcord: build: . volumes: - ./config.yaml:/llmcord/config.yaml restart: unless-stopped ``` -------------------------------- ### Bot Ready Event Handler Source: https://context7.com/jakobdylanc/llmcord/llms.txt Logs the bot invite URL and syncs the slash command tree upon successful connection to Discord. Requires `client_id` from configuration. ```python @discord_bot.event async def on_ready() -> None: if client_id := config.get("client_id"): logging.info( f"\n\nBOT INVITE URL:\n" f"https://discord.com/oauth2/authorize" f"?client_id={client_id}&permissions=412317191168&scope=bot\n" ) await discord_bot.tree.sync() # Log output: # BOT INVITE URL: # https://discord.com/oauth2/authorize?client_id=123456789&permissions=412317191168&scope=bot ``` -------------------------------- ### llmcord Configuration (`config.yaml`) Source: https://context7.com/jakobdylanc/llmcord/llms.txt Defines bot settings, LLM provider details, and model configurations. Load this file hot on every message; no restart is required. ```yaml # Discord settings bot_token: "YOUR_DISCORD_BOT_TOKEN" client_id: 123456789012345678 status_message: "Ask me anything!" max_text: 100000 # Max characters per message (including file attachments) max_images: 5 # Max image attachments per message (vision models only) max_messages: 25 # Max messages in a reply chain before oldest are dropped use_plain_responses: false # true = plaintext (no embeds, no streaming) allow_dms: true # false = disable direct message access permissions: users: admin_ids: [111111111111111111] # Can always use /model and DM the bot allowed_ids: [] # Empty = allow all users blocked_ids: [222222222222222222] roles: allowed_ids: [] blocked_ids: [] channels: allowed_ids: [] # Empty = allow all channels blocked_ids: [333333333333333333] # LLM provider and model settings providers: openai: base_url: https://api.openai.com/v1 api_key: sk-... google: base_url: https://generativelanguage.googleapis.com/v1beta/openai api_key: AIza... groq: base_url: https://api.groq.com/openai/v1 api_key: gsk_... mistral: base_url: https://api.mistral.ai/v1 api_key: ... openrouter: base_url: https://openrouter.ai/api/v1 api_key: sk-or-... x-ai: base_url: https://api.x.ai/v1 api_key: ... # Azure OpenAI (extra_query required for api-version) azure-openai: base_url: https://my-resource.openai.azure.com/openai/deployments/my-deployment api_key: ... extra_query: api-version: 2024-12-01-preview # Local providers (no api_key needed) ollama: base_url: http://localhost:11434/v1 lmstudio: base_url: http://localhost:1234/v1 vllm: base_url: http://localhost:8000/v1 models: # First model in the list is the default at startup openai/gpt-5: reasoning_effort: high verbosity: medium openrouter/x-ai/grok-4-fast:online: # No extra params needed ollama/llama4: # Local model via Ollama google/gemini-2.5-pro:vision: # Append :vision to enable image support system_prompt: | You are a helpful Discord assistant. Today's date is {date}. The current time is {time}. User messages are prefixed with their Discord ID as <@ID>. Use this format to mention users. ``` -------------------------------- ### Message Formatting for LLM Source: https://context7.com/jakobdylanc/llmcord/llms.txt Demonstrates different message formats for LLM interaction, including text, image URLs, and system prompts. The content can be a string for text or a list for multimodal inputs. ```python messages = [ {"role": "user", "content": "<@111>: What's the weather?"}, {"role": "assistant", "content": "I don't have real-time data, but..."}, {"role": "user", "content": "<@222>: Can you write a poem?"}, # Vision model: content becomes a list {"role": "user", "content": [ {"type": "text", "text": "<@333>: What's in this image?"}, {"type": "image_url", "image_url": {"url": "data:image/png;base64,iVBOR..."}} ]}, {"role": "system", "content": "You are a helpful assistant. Today is July 15 2025."} ] ``` -------------------------------- ### Slash Command Handler for Model Switching Source: https://context7.com/jakobdylanc/llmcord/llms.txt Handles the `/model` command, allowing admins to switch the active LLM. Non-admins can only view the current model. Responses are ephemeral in DMs. ```python # Registered automatically when the bot starts via discord_bot.tree.sync() # Users invoke it in Discord as: /model # Admin switching model: # /model → autocomplete shows: # ◉ openai/gpt-5 (current) # ○ ollama/llama4 # ○ google/gemini-2.5-pro:vision # Result for admin: "Model switched to: `ollama/llama4`" # Result for non-admin: "You don't have permission to change the model." # The underlying handler: async def model_command(interaction: discord.Interaction, model: str) -> None: global curr_model if model == curr_model: output = f"Current model: `{curr_model}`" else: if interaction.user.id in config["permissions"]["users"]["admin_ids"]: curr_model = model output = f"Model switched to: `{model}`" else: output = "You don't have permission to change the model." await interaction.response.send_message( output, ephemeral=(interaction.channel.type == discord.ChannelType.private) ) ``` -------------------------------- ### Hot-Reloading Configuration Loader Source: https://context7.com/jakobdylanc/llmcord/llms.txt Loads configuration from a YAML file. Use `asyncio.to_thread` for non-blocking calls within async functions to enable hot-reloading. ```python import yaml from typing import Any def get_config(filename: str = "config.yaml") -> dict[str, Any]: with open(filename, encoding="utf-8") as file: return yaml.safe_load(file) # Synchronous usage at startup config = get_config() print(config["models"]) # {'openai/gpt-5': {'reasoning_effort': 'high'}, 'ollama/llama4': None} # Async hot-reload inside an event handler (non-blocking) import asyncio config = await asyncio.to_thread(get_config) print(config.get("system_prompt")) # 'You are a helpful Discord assistant.\n...' ``` -------------------------------- ### Dynamic Model Autocomplete for Slash Command Source: https://context7.com/jakobdylanc/llmcord/llms.txt Provides suggestions for the `/model` command's model argument. Reloads config on empty input and prioritizes the current model. ```python @model_command.autocomplete("model") async def model_autocomplete(interaction: discord.Interaction, curr_str: str) -> list[Choice[str]]: global config if curr_str == "": config = await asyncio.to_thread(get_config) # Hot reload on empty input choices = ( [Choice(name=f"◉ {curr_model} (current)", value=curr_model)] if curr_str.lower() in curr_model.lower() else [] ) choices += [ Choice(name=f"○ {model}", value=model) for model in config["models"] if model != curr_model and curr_str.lower() in model.lower() ] return choices[:25] # Discord limits autocomplete to 25 choices # When user types "gpt" in /model: # → [Choice(name="○ openai/gpt-5", value="openai/gpt-5")] ``` -------------------------------- ### Core Message Handler Logic Source: https://context7.com/jakobdylanc/llmcord/llms.txt Processes incoming Discord messages, handling permissions, building conversation history, and managing LLM responses. Supports DMs and server mentions. ```python # Trigger: @ mention the bot in a server, or send any DM # --- Permission resolution --- # config.permissions.users.admin_ids → always allowed # config.permissions.users.allowed_ids → empty = allow all # config.permissions.channels.blocked_ids → explicitly denied # --- Reply chain traversal --- # Walks backwards through Discord reply references, up to max_messages # Each node is locked with asyncio.Lock to prevent duplicate fetches ``` -------------------------------- ### MsgNode Dataclass for Message Caching Source: https://context7.com/jakobdylanc/llmcord/llms.txt Caches relevant data for a Discord message, including text, images, and parent message references. Uses an asyncio.Lock to prevent concurrent processing. ```python from dataclasses import dataclass, field from typing import Any, Literal, Optional import asyncio import discord @dataclass class MsgNode: role: Literal["user", "assistant"] = "assistant" text: Optional[str] = None # Extracted message text (content + embeds + text files) images: list[dict[str, Any]] = field(default_factory=list) # Base64-encoded images has_bad_attachments: bool = False # True if unsupported attachments were present fetch_parent_failed: bool = False # True if parent message could not be fetched parent_msg: Optional[discord.Message] = None # Next message up in the reply chain lock: asyncio.Lock = field(default_factory=asyncio.Lock) # Prevents race conditions # Usage: nodes are stored in a global dict keyed by Discord message ID msg_nodes: dict[int, MsgNode] = {} ``` -------------------------------- ### Streamed Reply Helper Function Source: https://context7.com/jakobdylanc/llmcord/llms.txt An asynchronous helper function to send or continue bot replies, managing message chaining and node locking for streaming responses. It ensures the message node remains locked during the streaming process. ```python async def reply_helper(**reply_kwargs) -> None: reply_target = new_msg if not response_msgs else response_msgs[-1] response_msg = await reply_target.reply(**reply_kwargs) response_msgs.append(response_msg) msg_nodes[response_msg.id] = MsgNode(parent_msg=new_msg) await msg_nodes[response_msg.id].lock.acquire() # Lock is released in the outer scope after streaming completes: # msg_nodes[response_msg.id].lock.release() ``` ```python # Called with embed for streaming mode: await reply_helper(embed=discord.Embed(description="Thinking... ⚪"), silent=True) ``` ```python # Called with LayoutView for plain text mode: from discord.ui import LayoutView, TextDisplay await reply_helper(view=LayoutView().add_item(TextDisplay(content="Response text"))) ``` === COMPLETE CONTENT === This response contains all available snippets from this library. No additional content exists. Do not make further requests.