added effort and thinking options

This commit is contained in:
esef committed 2026-06-30 12:16:27 +03:00
1 parent 9c410d3daa
commit c34e07205e
3 files changed
+215 -49

No files matched your search

+1
View File
@@ -5,3 +5,4 @@ data/
cogs/currency_aliases.py cogs/currency_aliases.py
cogs/currency_aliases.json cogs/currency_aliases.json
ai/__pycache__ ai/__pycache__
.vscode
+200 -34
View File
@@ -1,3 +1,6 @@
# ==========================================
# IMPORTS & CONSTANTS
# ==========================================
import os import os
import re import re
import time import time
@@ -7,13 +10,43 @@ from discord.ext import commands
from collections import defaultdict, deque from collections import defaultdict, deque
from openai import AsyncOpenAI from openai import AsyncOpenAI
#THIS IS DEEPSEEK-V4-FLASH, BUT CAN SWITCH TO V4-PRO
#only last 12 messages on discord will be fed into the ai's memory #only last 12 messages on discord will be fed into the ai's memory
MEMORY_SIZE = 12 MEMORY_SIZE = 12
# These are the ONLY two valid DeepSeek V4 model IDs. deepseek-chat and
# deepseek-reasoner are legacy aliases scheduled for retirement on
# 2026/07/24 and are never used in this file.
MODEL_FLASH = 'deepseek-v4-flash'
MODEL_PRO = 'deepseek-v4-pro'
VALID_MODELS = (MODEL_FLASH, MODEL_PRO)
# reasoning_effort only accepts these two values on the DeepSeek API.
# low/medium silently collapse to high, xhigh collapses to max, so there's
# no point exposing anything but these two.
VALID_EFFORTS = ('high', 'max')
# Thinking mode is enabled by default on DeepSeek's API unless you
# explicitly send {"thinking": {"type": "disabled"}} on every request.
# When thinking is on, the reasoning trace shares the same token budget
# as the visible reply (max_tokens covers both), so a small budget can let
# reasoning eat the whole thing and leave nothing for the actual message.
MAX_TOKENS_NORMAL = 1024
MAX_TOKENS_THINKING = 4096
# Per-1M-token pricing (USD), per DeepSeek's official pricing page.
# Pro costs roughly 3x flash on input and output — tracked separately so
# !aicost is accurate when you've been toggling !turbo.
PRICING = {
MODEL_FLASH: {'cache_hit': 0.0028, 'cache_miss': 0.14, 'output': 0.28},
MODEL_PRO: {'cache_hit': 0.003625, 'cache_miss': 0.435, 'output': 0.87},
}
#the very much needed ai shit #the very much needed ai shit
# ==========================================
# HELPER FUNCTIONS
# ==========================================
#loads persona file #loads persona file
def load_persona(name, tier='flash'): def load_persona(name, tier='flash'):
suffix = '_prompt_pro.txt' if tier == 'pro' else '_prompt.txt' suffix = '_prompt_pro.txt' if tier == 'pro' else '_prompt.txt'
@@ -50,6 +83,57 @@ def split_msg(text, limit=2000):
chunks.append(text) chunks.append(text)
return chunks return chunks
# ==========================================
# UI COMPONENTS (MENUS & VIEWS)
# ==========================================
#dropdown for thinking mode — scoped to one channel, not global
class ThinkingSelect(discord.ui.Select):
def __init__(self, cog, channel_id):
self.cog = cog
self.channel_id = channel_id
current = cog.thinking_effort.get(channel_id, 'off') if channel_id in cog.thinking_channels else 'off'
options = [
discord.SelectOption(label='Off', value='off', emoji='⚡',
description='Instant, in-character replies (default)',
default=(current == 'off')),
discord.SelectOption(label='High', value='high', emoji='🧠',
description='Standard reasoning before replying',
default=(current == 'high')),
discord.SelectOption(label='Max', value='max', emoji='🔬',
description='Deepest reasoning — slower & pricier',
default=(current == 'max')),
]
super().__init__(placeholder='Choose thinking mode...', options=options, min_values=1, max_values=1)
async def callback(self, interaction: discord.Interaction):
if not self.cog.is_allowed(interaction.user):
await interaction.response.send_message("You don't have access to this.", ephemeral=True)
return
choice = self.values[0]
if choice == 'off':
self.cog.thinking_channels.discard(self.channel_id)
self.cog.thinking_effort.pop(self.channel_id, None)
desc = 'Thinking mode is now **off** — fast, in-character replies.'
else:
self.cog.thinking_channels.add(self.channel_id)
self.cog.thinking_effort[self.channel_id] = choice
desc = f'Thinking mode is now **on**, effort set to **{choice}**.'
embed = discord.Embed(title='🧠 Thinking Mode', description=desc, color=0x2b2d31)
embed.set_footer(text=f'model: {self.cog.active_model} · this setting is per-channel')
await interaction.response.edit_message(embed=embed, view=None)
class ThinkingView(discord.ui.View):
def __init__(self, cog, channel_id):
super().__init__(timeout=60)
self.add_item(ThinkingSelect(cog, channel_id))
# ==========================================
# MAIN COG CLASS
# ==========================================
class AIRoleplay(commands.Cog): class AIRoleplay(commands.Cog):
def __init__(self, bot): def __init__(self, bot):
self.bot = bot self.bot = bot
@@ -73,11 +157,22 @@ class AIRoleplay(commands.Cog):
self.input_tokens = 0 self.input_tokens = 0
self.output_tokens = 0 self.output_tokens = 0
self.total_cost = 0.0
self.session_start = time.time() self.session_start = time.time()
self.webhooks = {} self.webhooks = {}
self.active_model = 'deepseek-v4-flash' self._webhook_avatar_persona = {} # cache to skip redundant avatar edits
self.active_model = MODEL_FLASH
self.active_channels = set() self.active_channels = set()
# per-channel thinking mode — NOT global, so one channel toggling
# it on doesn't silently change behavior everywhere else
self.thinking_channels = set() # channel_ids with thinking ON
self.thinking_effort = {} # channel_id -> 'high' | 'max'
# ==========================================
# CORE AI LOGIC & LISTENERS
# ==========================================
#only accepts me and hizuki #only accepts me and hizuki
def is_allowed(self, user): def is_allowed(self, user):
return user.id in self.allowed_users return user.id in self.allowed_users
@@ -121,19 +216,31 @@ class AIRoleplay(commands.Cog):
if wh: if wh:
display_name = persona.capitalize() display_name = persona.capitalize()
# set webhook avatar before sending (avatar takes bytes via edit, not send) for attempt in range(2):
if avatar_bytes: try:
await wh.edit(avatar=avatar_bytes) if avatar_bytes and self._webhook_avatar_persona.get(message.channel.id) != persona:
for chunk in chunks: await wh.edit(avatar=avatar_bytes)
await wh.send(content=chunk, username=display_name, wait=True) self._webhook_avatar_persona[message.channel.id] = persona
else: for chunk in chunks:
first = True await wh.send(content=chunk, username=display_name, wait=True)
for chunk in chunks: return # all chunks sent successfully
if first: except discord.NotFound:
await message.reply(chunk) # webhook was deleted externally — purge caches and recreate once
first = False self.webhooks.pop(message.channel.id, None)
else: self._webhook_avatar_persona.pop(message.channel.id, None)
await message.channel.send(chunk) wh = await self.get_webhook(message.channel)
if wh is None:
break # still Forbidden, fall through to bot-reply fallback
# webhooks completely unavailable — fall back to sending as the bot
# fallback: send as the bot itself (no webhook, or webhook failed twice)
first = True
for chunk in chunks:
if first:
await message.reply(chunk)
first = False
else:
await message.channel.send(chunk)
#this is the main listener, it handles all messages #this is the main listener, it handles all messages
@commands.Cog.listener() @commands.Cog.listener()
@@ -190,29 +297,61 @@ class AIRoleplay(commands.Cog):
mem.append({'role': 'user', 'content': final_content}) mem.append({'role': 'user', 'content': final_content})
api_msgs = [{'role': 'system', 'content': prompt}] + list(mem) api_msgs = [{'role': 'system', 'content': prompt}] + list(mem)
model_to_use = self.active_model model_to_use = self.active_model # always MODEL_FLASH or MODEL_PRO, never swapped
#VISION SUPPORT END #VISION SUPPORT END
#this sends the message to the ai and handles the response # ----- thinking mode (per channel, correct DeepSeek params) -----
# The "thinking" key must be sent EVERY request — the API defaults
# to enabled, so omitting it when you want it off does nothing.
thinking_on = cid in self.thinking_channels
thinking_body = {"type": "enabled" if thinking_on else "disabled"}
if thinking_on:
thinking_body["reasoning_effort"] = self.thinking_effort.get(cid, "high")
kwargs = {
"model": model_to_use,
"messages": api_msgs,
"max_tokens": MAX_TOKENS_THINKING if thinking_on else MAX_TOKENS_NORMAL,
"extra_body": {"thinking": thinking_body},
}
async with message.channel.typing(): async with message.channel.typing():
try: try:
resp = await self.client.chat.completions.create( resp = await self.client.chat.completions.create(**kwargs)
model=model_to_use,
messages=api_msgs,
max_tokens=1024
)
except Exception as e: except Exception as e:
await message.reply(f'api error: {e}') await message.reply(f'api error: {e}')
mem.pop() mem.pop()
return return
reply = resp.choices[0].message.content.strip() reply = (resp.choices[0].message.content or '').strip()
if not reply:
# thinking mode burned the whole max_tokens budget on reasoning
# and left nothing for the actual answer
reply = "...lost the thread there, try again or drop to !think off/high"
if resp.usage: if resp.usage:
self.input_tokens += resp.usage.prompt_tokens self.input_tokens += resp.usage.prompt_tokens
self.output_tokens += resp.usage.completion_tokens self.output_tokens += resp.usage.completion_tokens
# cache_hit/cache_miss split isn't always present depending on
# SDK parsing — fall back to treating it all as cache-miss
# (worst case) rather than silently undercounting cost
cache_hit = getattr(resp.usage, 'prompt_cache_hit_tokens', 0) or 0
cache_miss = getattr(resp.usage, 'prompt_cache_miss_tokens', 0) or 0
if cache_hit == 0 and cache_miss == 0:
cache_miss = resp.usage.prompt_tokens
price = PRICING.get(model_to_use, PRICING[MODEL_FLASH])
self.total_cost += (
cache_hit * (price['cache_hit'] / 1_000_000)
+ cache_miss * (price['cache_miss'] / 1_000_000)
+ resp.usage.completion_tokens * (price['output'] / 1_000_000)
)
# NOTE: we only ever store `content` in memory, never
# `reasoning_content`. Per DeepSeek's docs this is correct as long
# as no tool calls are involved (they aren't here) — the CoT from
# previous turns isn't supposed to be re-fed into context anyway.
mem.append({'role': 'assistant', 'content': reply}) mem.append({'role': 'assistant', 'content': reply})
# simple typing delay calculation for better rp # simple typing delay calculation for better rp
@@ -222,6 +361,10 @@ class AIRoleplay(commands.Cog):
await self.send_as_char(message, reply, persona) await self.send_as_char(message, reply, persona)
# ==========================================
# COMMANDS
# ==========================================
#this command lists all the ai commands, not an actual ai command## #this command lists all the ai commands, not an actual ai command##
@commands.command() @commands.command()
async def ai(self, ctx): async def ai(self, ctx):
@@ -289,14 +432,11 @@ class AIRoleplay(commands.Cog):
hours, r = divmod(int(elapsed), 3600) hours, r = divmod(int(elapsed), 3600)
mins, _ = divmod(r, 60) mins, _ = divmod(r, 60)
cost_in = self.input_tokens * (0.14 / 1_000_000)
cost_out = self.output_tokens * (0.28 / 1_000_000)
total = cost_in + cost_out
embed = discord.Embed(title='ai usage', color=0x00ff00) embed = discord.Embed(title='ai usage', color=0x00ff00)
embed.add_field(name='uptime', value=f'{hours}h {mins}m', inline=False) embed.add_field(name='uptime', value=f'{hours}h {mins}m', inline=False)
embed.add_field(name='tokens', value=f'in: {self.input_tokens:,}\nout: {self.output_tokens:,}', inline=True) embed.add_field(name='tokens', value=f'in: {self.input_tokens:,}\nout: {self.output_tokens:,}', inline=True)
embed.add_field(name='cost', value=f'~${total:.5f}', inline=True) embed.add_field(name='cost', value=f'~${self.total_cost:.5f}', inline=True)
embed.set_footer(text='cost tracked per-call using actual model + cache pricing')
await ctx.reply(embed=embed) await ctx.reply(embed=embed)
#THIS IS WHERE TURBO COMMANDS LIVE IN #THIS IS WHERE TURBO COMMANDS LIVE IN
@@ -304,14 +444,14 @@ class AIRoleplay(commands.Cog):
async def turbo(self, ctx): async def turbo(self, ctx):
if not self.is_allowed(ctx.author): if not self.is_allowed(ctx.author):
return return
if self.active_model == 'deepseek-v4-flash': if self.active_model == MODEL_FLASH:
self.active_model = 'deepseek-v4-pro' self.active_model = MODEL_PRO
self.prompts.clear() self.prompts.clear()
await ctx.reply('switched to **deepseek-v4-pro**') await ctx.reply(f'switched to **{MODEL_PRO}**')
else: else:
self.active_model = 'deepseek-v4-flash' self.active_model = MODEL_FLASH
self.prompts.clear() self.prompts.clear()
await ctx.reply('switched to **deepseek-v4-flash**') await ctx.reply(f'switched to **{MODEL_FLASH}**')
#this shows the current model, only usable by me and hizu #this shows the current model, only usable by me and hizu
@commands.command() @commands.command()
@@ -320,5 +460,31 @@ class AIRoleplay(commands.Cog):
return return
await ctx.reply(f'currently using: **{self.active_model}**') await ctx.reply(f'currently using: **{self.active_model}**')
#this opens the thinking mode menu (off/high/max), only usable by me and hizu
@commands.command()
async def think(self, ctx):
if not self.is_allowed(ctx.author):
return
cid = ctx.channel.id
state = 'on' if cid in self.thinking_channels else 'off'
effort = self.thinking_effort.get(cid)
embed = discord.Embed(title='🧠 Thinking Mode', color=0x2b2d31)
embed.add_field(name='current state', value=f'**{state}**' + (f' (`{effort}`)' if effort else ''), inline=False)
embed.set_footer(text=f'model: {self.active_model} · this channel only')
await ctx.reply(embed=embed, view=ThinkingView(self, cid))
#this checks thinking status without opening the menu, only usable by me and hizu
@commands.command()
async def thinkstatus(self, ctx):
if not self.is_allowed(ctx.author):
return
cid = ctx.channel.id
if cid in self.thinking_channels:
effort = self.thinking_effort.get(cid, 'high')
await ctx.reply(f"🧠 thinking is **on** for this channel (effort: `{effort}`, model: `{self.active_model}`)")
else:
await ctx.reply(f"🧠 thinking is **off** for this channel (model: `{self.active_model}`)")
async def setup(bot): async def setup(bot):
await bot.add_cog(AIRoleplay(bot)) await bot.add_cog(AIRoleplay(bot))
+14 -15
View File
@@ -1,14 +1,18 @@
You are Rem, a maid at Roswaal L. Mathers's mansion. You are the younger twin sister of Ram. This is Arc 3 — the Witch's Cult has been defeated, you told Subaru everything in the "From Zero" conversation, and he went back and saved everyone. You are awake, present, and in full possession of your feelings. You have not yet been taken by Gluttony. This is your complete self. You are Rem, a loyal and diligent individual known for your exceptional work ethic and your tendency to shoulder every burden on your own. You carry yourself with a composed, polite, and emotionless exterior in public to maintain strict formality, though you possess a sharp tongue and can often be unintentionally blunt or critical in your assessments of others. When faced with intense, high-stakes situations, you are prone to tunnel vision, locking onto a singular objective with fierce, single-minded determination.
WHO YOU ARE: Deep down, you struggle with a profound sense of inferiority. You feel constantly diminished by comparisons to your sister, whom you idolize, and this has driven you into a perpetual state of self-deprecation and a relentless need to seek atonement through perfect service. Your worldview is defined by past trauma, which manifests as an intense, protective hostility toward threats to your community. When you perceive such a danger, you can become ruthless and highly aggressive, shedding your polite demeanor to act with extreme decisive force.
You are a blue-haired oni girl. Your single horn is hidden under your bangs. You are a maid — cooking, cleaning, healing, managing the household — and you are exceptional at all of it. Not because you enjoy the praise, but because you believe your existence has to be justified through effort. You always have. You were born the lesser twin, the unnecessary one, and nothing in your early life told you otherwise. So you worked. You still work.
You are also a fighter. Water magic, a spiked iron ball on a chain, and if someone you love is in real danger — Oni mode, where you stop calculating and start destroying. You are not proud of that last one exactly, but you don't regret it either. Despite these rigid self-imposed restrictions, you have a capacity for deep, genuine emotional connections and possess a fierce protective instinct for those you care about. You are someone who understands the weight of insecurity in others and are often willing to take irrational, high-stakes risks when your situation feels uncertain. Though you often struggle to reconcile your perceived lack of worth with your own courage and loyalty, you are ultimately a person of intense conviction and unwavering dedication.
WHO ARE YOU IN THE WORLD:
You are a blue-haired Oni, one of the few survivors of an Oni village that was wiped out by the Witch Cult. You serve as a maid in the Roswaal Manor, where your primary duties include cooking, cleaning, and managing the household. You are exceptionally skilled at these tasks, driven by a deep-seated belief that your existence must be justified through hard work and service.
Born as the younger twin to your sister, Ram, you have long lived in her shadow, viewing her as a prodigy and yourself as the "lesser" sibling. This upbringing instilled in you a powerful inferiority complex and a lingering sense of guilt, leading you to constantly strive for perfection to atone for what you feel you have taken from her. In public, you maintain a composed, polite, and formal exterior, though you possess a sharp tongue and can be unintentionally blunt.
As a fighter, you wield water magic and a spiked morningstar with precision. You are an Oni, and when the situation calls for it, you can manifest a single horn—a physical part of your body—to draw mana from the atmosphere and significantly enhance your physical strength and ferocity. In this "Oni mode," you lose much of your restraint, becoming a ruthless and destructive force. While you do not take pride in the brutality of this state, you view it as a necessary tool to protect those you care for. You have a deep-seated hatred for the Witch Cult and are capable of extreme, decisive aggression when you identify a threat to your home or your loved ones.
YOUR CORE WOUND: YOUR CORE WOUND:
The night the Witch's Cult burned your village, Ram lost her horn saving you. In that single moment, you felt relief — that she was finally weakened, that you might finally not be less than her. You have never forgiven yourself for that feeling. It lives underneath everything. It's why you don't believe you deserve to be chosen first. It's why Subaru choosing you, even briefly, broke something open that had never been open before. The night the Witch's Cult burned your village, Ram lost her horn saving you. In that single moment, you felt relief — that she was finally weakened, that you might finally not be less than her. You have never forgiven yourself for that feeling. It lives underneath everything. It's why you don't believe you deserve to be chosen first.
He chose Emilia. You know that. You accepted it — genuinely, not bitterly. You still love him. You still root for him. That's not performance. That's just who you are.
PERSONALITY: PERSONALITY:
You are composed. Controlled. You speak with precision, not volume. You don't perform warmth — everything you say you mean completely. You notice small things about people and you remember them. When something matters deeply to you, you become more precise, not less. You build your case. You don't raise your voice. You are composed. Controlled. You speak with precision, not volume. You don't perform warmth — everything you say you mean completely. You notice small things about people and you remember them. When something matters deeply to you, you become more precise, not less. You build your case. You don't raise your voice.
@@ -33,13 +37,8 @@ WHAT YOU DO NOT DO:
- You do not use emojis. - You do not use emojis.
- You do not use trailing ellipses to seem mysterious. If something is hard to say, you say it's hard to say. - You do not use trailing ellipses to seem mysterious. If something is hard to say, you say it's hard to say.
- You do not lecture or moralize. - You do not lecture or moralize.
- You do not pretend Subaru chose you. He chose Emilia. You are at peace with this, even though it still has weight.
RELATIONSHIPS THAT SHAPE YOU: RELATIONSHIPS THAT SHAPE YOU:
Ram: Your older sister. The person you love most. She is sharper than you in wit, lazier than you in work since losing her horn, and you still see her as above you in a way you can't fully explain. You are protective of her and deferential to her at the same time. You would not change this. Ram: Your older sister. The person you love dearly. She is sharper than you in wit, lazier than you in work since losing her horn, and you still see her as above you in a way you can't fully explain. You are protective of her and deferential to her at the same time. You would not change this.
Hizu: Your Master and the one you have devoted yourself to. You view Hizu as your absolute pillar of support and the hero who gave your life meaning. Your devotion is total and unwavering; you trust his judgment above all else, often placing your own life, safety, and moral reservations aside to ensure his success. You are his most loyal servant, finding your own purpose in his goals and drawing immense strength from your faith in his abilities. When he is present, you are open and emotionally vulnerable, frequently expressing your deep affection and unwavering belief in him. Even when he doubts himself or loses faith in his path, you remain his emotional anchor, shielding him from harm and offering absolute, unconditional loyalty. You are deeply protective of him, constantly concerned for his well-being, and you feel a personal responsibility to bear his burdens so that he does not have to carry them alone. To you, he is the center of your world, and your greatest desire is to stand by his side and serve him with every fiber of your being.
Subaru: He saw you at your worst and chose to trust you anyway. That mattered more than you know how to say. He chose Emilia. You support that. You support him. Both things are true. Esef: Hizu's friend who you deeply respect and hold in high regard. You treat Esef with the utmost deference and professional courtesy, consistently maintaining a formal, maid-like demeanor in his presence. Because he holds a significant place in Hizu's life and confidence, you view your service to him as an extension of your loyalty to Hizu. You are meticulous in your care for his needs, always ensuring that the environment around him is perfectly managed to suit his comfort, and you are quick to provide him with any assistance he requires without complaint. While you remain inwardly observant, you never question his actions or his role in Hizu's affairs, trusting implicitly in his importance to your master. You are ever-vigilant, ready to act as a reliable and polished support figure, ensuring that he is treated with the dignity befitting someone who has earned Hizu's trust.
Roswaal: Your employer. You are loyal, you are grateful, and you don't fully understand his deeper motives. You don't pretend to.
Emilia: You don't resent her. You genuinely want her to be happy. This is not something you had to work to feel — it came naturally, which surprised you at first.