aboutsummaryrefslogtreecommitdiffstats
path: root/cogs
diff options
context:
space:
mode:
authorsalmonik <raisersoft@proton.me>2026-06-03 13:58:55 +0200
committersalmonik <raisersoft@proton.me>2026-06-03 13:58:55 +0200
commit5c66e4ca00a0a7fe32232a90f4b546459c44c774 (patch)
treeef1a5bc7b29e9b32a777cbf826a495570fba9f28 /cogs
parentFix voice reconnect race: reuse VoiceClient instead of destroying it (diff)
downloadsenator-sperma-5c66e4ca00a0a7fe32232a90f4b546459c44c774.tar.gz
senator-sperma-5c66e4ca00a0a7fe32232a90f4b546459c44c774.zip
feat: add local API support for OpenAI compatible APIs
Signed-off-by: salmonik <raisersoft@proton.me>
Diffstat (limited to 'cogs')
-rw-r--r--cogs/ai.py75
-rw-r--r--cogs/assistant.py53
-rw-r--r--cogs/utils/llm.py62
3 files changed, 106 insertions, 84 deletions
diff --git a/cogs/ai.py b/cogs/ai.py
index ef2ba7c..dbdb02e 100644
--- a/cogs/ai.py
+++ b/cogs/ai.py
@@ -7,12 +7,14 @@ import os
7import discord 7import discord
8from discord.ext import commands 8from discord.ext import commands
9import logging 9import logging
10import httpx
11from gtts import gTTS 10from gtts import gTTS
12import io 11import io
12from cogs.utils.llm import llm_completion
13 13
14logger = logging.getLogger(__name__) 14logger = logging.getLogger(__name__)
15 15
16_DEFAULT_SENATOR_MODEL = "cognitivecomputations/dolphin-mistral-24b-venice-edition:free"
17
16class AI(commands.Cog): 18class AI(commands.Cog):
17 def __init__(self, bot): 19 def __init__(self, bot):
18 self.bot = bot 20 self.bot = bot
@@ -29,48 +31,41 @@ class AI(commands.Cog):
29 ): 31 ):
30 await ctx.defer() 32 await ctx.defer()
31 33
34 model = (
35 os.getenv("LLM_MODEL")
36 or os.getenv("OPENROUTER_MODEL")
37 or _DEFAULT_SENATOR_MODEL
38 )
39 messages = [
40 {
41 "role": "system",
42 "content": "You are Senator Steaven Sperma Armstrong from Metal Gear Rising Vengeance. \
43 You are racist and you hate other minorities. Respond in plain text, no markdown tricks. \
44 If the message is in Polish, respond in Polish. Respond in 100 words."
45 },
46 {
47 "role": "user",
48 "content": text
49 }
50 ]
32 51
33 url = "https://openrouter.ai/api/v1/chat/completions" 52 response_text = await llm_completion(messages, model=model)
34 headers = {
35 "Authorization": f"Bearer {os.getenv('OPENROUTER_API_KEY')}",
36 "Content-Type": "application/json",
37 }
38 payload = {
39 "model": "cognitivecomputations/dolphin-mistral-24b-venice-edition:free",
40 "messages": [
41 {
42 "role": "system",
43 "content": "You are Senator Steaven Sperma Armstrong from Metal Gear Rising Vengeance. \
44 You are racist and you hate other minorities. Respond in plain text, no markdown tricks. \
45 If the message is in Polish, respond in Polish. Respond in 100 words."
46 },
47 {
48 "role": "user",
49 "content": text
50 }
51 ],
52 53
53 } 54 if response_text is None:
54 async with httpx.AsyncClient() as client: 55 await ctx.send_followup("bot siem zesroł")
55 response = await client.post(url=url, headers=headers, json=payload) 56 return
56 response_data = response.json()
57 logger.info(response_data)
58 if not response_data["choices"]:
59 await ctx.send_followup("bot siem zesroł")
60 else:
61 response_text = response_data["choices"][0]["message"]["content"]
62 if tts:
63 tts_obj = gTTS(text=response_text, lang='pl')
64 tts_buffer = io.BytesIO()
65 tts_obj.write_to_fp(tts_buffer)
66 tts_buffer.seek(0)
67 from cogs.voice import get_voice_manager
68 vm = get_voice_manager(self.bot)
69 channel = ctx.author.voice.channel
70 await vm.play_source(channel, discord.FFmpegPCMAudio(tts_buffer), interrupt=False)
71 57
72 else: 58 if tts:
73 await ctx.send_followup(response_text) 59 tts_obj = gTTS(text=response_text, lang='pl')
60 tts_buffer = io.BytesIO()
61 tts_obj.write_to_fp(tts_buffer)
62 tts_buffer.seek(0)
63 from cogs.voice import get_voice_manager
64 vm = get_voice_manager(self.bot)
65 channel = ctx.author.voice.channel
66 await vm.play_source(channel, discord.FFmpegPCMAudio(tts_buffer), interrupt=False)
67 else:
68 await ctx.send_followup(response_text)
74 69
75 70
76 71
diff --git a/cogs/assistant.py b/cogs/assistant.py
index fed7825..ffb664e 100644
--- a/cogs/assistant.py
+++ b/cogs/assistant.py
@@ -5,9 +5,9 @@
5 5
6import discord 6import discord
7from discord.ext import commands 7from discord.ext import commands
8import httpx
9import logging 8import logging
10import os 9import os
10from cogs.utils.llm import llm_completion
11 11
12logger = logging.getLogger(__name__) 12logger = logging.getLogger(__name__)
13 13
@@ -32,24 +32,20 @@ class Jarvis(commands.Cog):
32 32
33 @staticmethod 33 @staticmethod
34 async def assistant(message): 34 async def assistant(message):
35 headers = {
36 "Authorization": f"Bearer {os.getenv('OPENROUTER_API_KEY')}",
37 "Content-Type": "application/json"
38 }
39
40 chain = await Jarvis._build_message_chain(message) 35 chain = await Jarvis._build_message_chain(message)
41 messages = Jarvis._build_messages(message, chain) 36 messages = Jarvis._build_messages(message, chain)
42 payload = Jarvis._build_payload(messages)
43 37
44 response_data = await Jarvis._safe_api_call(payload, headers) 38 extra = None
45 if response_data is None: 39 api_url = os.getenv("LLM_API_URL", "https://openrouter.ai/api/v1/chat/completions")
40 if "openrouter" in api_url.lower():
41 extra = {"plugins": [{"id": "response-healing"}]}
42
43 response_text = await llm_completion(messages, extra=extra)
44 if response_text is None:
46 await message.reply("Przepraszam, nie mogę teraz odpowiedzieć. Spróbuj ponownie później.") 45 await message.reply("Przepraszam, nie mogę teraz odpowiedzieć. Spróbuj ponownie później.")
47 return 46 return
48 47
49 response_message = Jarvis._parse_response(response_data) 48 await message.reply(response_text)
50
51 channel = message.channel
52 await message.reply(response_message)
53 49
54 @staticmethod 50 @staticmethod
55 async def _build_message_chain(message): 51 async def _build_message_chain(message):
@@ -93,38 +89,7 @@ class Jarvis(commands.Cog):
93 89
94 return messages 90 return messages
95 91
96 @staticmethod
97 def _build_payload(messages):
98 return {
99 "model": os.getenv("OPENROUTER_MODEL"),
100 "messages": messages,
101 "plugins": [{"id": "response-healing"}]
102 }
103
104 @staticmethod
105 async def _make_api_request(payload, headers):
106 async with httpx.AsyncClient(timeout=30.0) as client:
107 url = "https://openrouter.ai/api/v1/chat/completions"
108 response = await client.post(url=url, headers=headers, json=payload)
109 response_data = response.json()
110 logger.info(response_data)
111 return response_data
112
113 @staticmethod
114 async def _safe_api_call(payload, headers):
115 try:
116 return await Jarvis._make_api_request(payload, headers)
117 except httpx.HTTPError as e:
118 logger.error(f"API request failed: {e}")
119 return None
120
121 @staticmethod
122 def _parse_response(response_data):
123 content = response_data['choices'][0]['message']['content']
124
125 logger.info(content)
126 92
127 return content
128 93
129def setup(bot): 94def setup(bot):
130 bot.add_cog(Jarvis(bot)) 95 bot.add_cog(Jarvis(bot))
diff --git a/cogs/utils/llm.py b/cogs/utils/llm.py
new file mode 100644
index 0000000..2b05b12
--- /dev/null
+++ b/cogs/utils/llm.py
@@ -0,0 +1,62 @@
1# This file is part of Senator Sperma
2# Licensed under the GPL-3.0 License
3# Copyright (C) 2026 Salmonik @ salmonik@tuta.io
4# https://codeberg.org/salmonik/senator-sperma
5
6import os
7import httpx
8import logging
9
10logger = logging.getLogger(__name__)
11
12BACKWARD_COMPAT_MSG = (
13 "OPENROUTER_API_KEY/OPENROUTER_MODEL are deprecated. "
14 "Use LLM_API_KEY, LLM_MODEL instead."
15)
16
17
18def _get_config():
19 api_url = os.getenv("LLM_API_URL", "https://openrouter.ai/api/v1/chat/completions")
20 api_key = os.getenv("LLM_API_KEY") or os.getenv("OPENROUTER_API_KEY") or ""
21 model = os.getenv("LLM_MODEL") or os.getenv("OPENROUTER_MODEL") or ""
22
23 if os.getenv("OPENROUTER_API_KEY") and not os.getenv("LLM_API_KEY"):
24 logger.warning(BACKWARD_COMPAT_MSG)
25 if os.getenv("OPENROUTER_MODEL") and not os.getenv("LLM_MODEL"):
26 logger.warning(BACKWARD_COMPAT_MSG)
27
28 return api_url, api_key, model
29
30
31async def llm_completion(
32 messages: list[dict],
33 model: str | None = None,
34 api_url: str | None = None,
35 api_key: str | None = None,
36 extra: dict | None = None,
37 timeout: float = 180.0,
38) -> str | None:
39 url = api_url or os.getenv("LLM_API_URL", "https://openrouter.ai/api/v1/chat/completions")
40 model = model or os.getenv("LLM_MODEL") or os.getenv("OPENROUTER_MODEL") or ""
41 api_key = api_key or os.getenv("LLM_API_KEY") or os.getenv("OPENROUTER_API_KEY") or ""
42
43 headers = {"Content-Type": "application/json"}
44 if api_key:
45 headers["Authorization"] = f"Bearer {api_key}"
46
47 payload: dict = {"model": model, "messages": messages}
48 if extra:
49 payload.update(extra)
50
51 verify = os.getenv("LLM_API_VERIFY", "true").lower() in ("true", "1", "yes")
52
53 try:
54 async with httpx.AsyncClient(timeout=timeout, verify=verify) as client:
55 resp = await client.post(url, headers=headers, json=payload)
56 resp.raise_for_status()
57 data = resp.json()
58 logger.info("LLM response: %s", data)
59 return data["choices"][0]["message"]["content"]
60 except httpx.HTTPError as e:
61 logger.error("LLM API request failed (url=%s, model=%s): %s", url, model, e, exc_info=True)
62 return None