From 9040fc7df9552c1723f8c7ee08b66f3577f351b3 Mon Sep 17 00:00:00 2001 From: ThePendulum Date: Sun, 18 Oct 2026 00:59:35 +0200 Subject: [PATCH] Updated OpenAI models for chat, using proper context prompt instead of prefixing message. --- config/default.js | 8 +++---- src/games/chat.js | 55 ++++++++++++++++++++++++++++++++--------------- 2 files changed, 42 insertions(+), 21 deletions(-) diff --git a/config/default.js b/config/default.js index 4526bd5..38d95c0 100755 --- a/config/default.js +++ b/config/default.js @@ -69,11 +69,11 @@ module.exports = { chat: { apiKey: null, // OpenAI validModels: [ - 'gpt-3.5-turbo', - 'text-davinci-003', - 'gpt-4', + 'gpt-6-luna', + 'gpt-6.1-sol', ], - model: 'gpt-3.5-turbo', + model: 'gpt-6-luna', + reasoningEffort: 'none', // none, low, medium, high; reasoning tokens count towards the user token limit userTokenLimit: 20000, // daily, roughly 100+ messages or $0.04 per user userTokenPeriod: 24, // hours replyTokenLimit: 1000, diff --git a/src/games/chat.js b/src/games/chat.js index 30b07f3..760befb 100644 --- a/src/games/chat.js +++ b/src/games/chat.js @@ -2,6 +2,7 @@ const config = require('config'); const bhttp = require('bhttp'); +const { format } = require('date-fns'); const style = require('../utils/style'); const knex = require('../knex'); @@ -145,6 +146,20 @@ function setRule(rule, context) { context.sendMessage(`Chat rule must be at least 3 characters long, ${context.user.prefixedUsername}`, context.room?.id, { label: false }, context.user.username); } +function getInstructions(context) { + return { + role: 'developer', + content: [ + `You are ${config.user.username}, ${settings.rule}, chatting in a group chat.`, + `You are talking to ${context.user.username}. Today is ${format(new Date(), 'EEEE d MMMM yyyy')}.`, + config.platform === 'irc' + ? 'Reply in plain text on a single line, IRC does not support any formatting.' + : 'Reply in a single paragraph without headings or lists. You may use **bold** and *italic*.', + settings.replyWordLimit && `Use ${settings.replyWordLimit} words or fewer.`, + ].filter(Boolean).join(' '), + }; +} + async function getTokens(username) { const { used_tokens: usedTokens } = await knex('chat_tokens') .sum('tokens as used_tokens') @@ -229,21 +244,17 @@ async function onCommand(args, context, isConversation) { return; } - const message = { - role: 'user', - content: settings.replyWordLimit - ? `Using ${settings.replyWordLimit} words or fewer, answer as if you're ${settings.rule}. ${prompt}` - : `Answer as if you're ${settings.rule}. ${prompt}`, - }; - + const message = { role: 'user', content: prompt }; const userHistory = (history.get(context.user.username) || []).concat(message); - context.logger.info(`${context.user.username} asked OpenAI '${config.chat.model}' (${userHistory.length}): ${message.content}`); + context.logger.info(`${context.user.username} asked OpenAI '${settings.model}' (${userHistory.length}) to be ${settings.rule}: ${prompt}`); const res = await bhttp.post('https://api.openai.com/v1/chat/completions', JSON.stringify({ model: settings.model, - max_tokens: config.chat.replyTokenLimit, - messages: userHistory, + max_completion_tokens: config.chat.replyTokenLimit, + reasoning_effort: config.chat.reasoningEffort, + temperature: settings.temperature, + messages: [getInstructions(context), ...userHistory], // instructions are rebuilt for every request and not stored in history, so rule changes apply immediately user: `${context.user.id}:${context.user.username}`, }), { headers: { @@ -252,11 +263,20 @@ async function onCommand(args, context, isConversation) { }, }); - const reply = res.body.choices?.[0].message; + if (res.statusCode !== 200) { + throw new Error(`OpenAI responded with ${res.statusCode}: ${res.body.error?.message || JSON.stringify(res.body)}`); + } + + const reply = res.body.choices?.[0]?.message; + + if (!reply?.content) { + throw new Error(`OpenAI returned no content (finish reason '${res.body.choices?.[0]?.finish_reason}', ${res.body.usage?.total_tokens} tokens)`); + } + const target = prompt.match(/@\s*[\w-]+\s*$/)?.[0].slice(1).trim(); const curatedContent = reply.content.replace(/\n+/g, '. '); - context.logger.info(`OpenAI ${config.chat.model} replied to ${target || context.user.username} (${res.body.usage.total_tokens} tokens, ${(usedTokens || 0) + res.body.usage.total_tokens}/${config.chat.userTokenLimit} used): ${curatedContent}`); + context.logger.info(`OpenAI ${settings.model} replied to ${target || context.user.username} (${res.body.usage.total_tokens} tokens, ${(usedTokens || 0) + res.body.usage.total_tokens}/${config.chat.userTokenLimit} used): ${curatedContent}`); if (isConversation) { context.sendMessage(`${curatedContent}`, context.room?.id, { label: false }, context.user.username); @@ -270,15 +290,16 @@ async function onCommand(args, context, isConversation) { created: knex.raw("datetime('now')"), }); - if (config.chat.history > 0) { - history.set(context.user.username, userHistory.concat(reply).slice(-settings.history)); + if (settings.history > 0) { + // only keep the fields the API accepts as input, newer models return extras like refusal and annotations + history.set(context.user.username, userHistory.concat({ role: reply.role, content: reply.content }).slice(-settings.history)); } } catch (error) { - context.logger.error(error.response ? `${error.response.status}: ${JSON.stringify(error.response.data)}` : error.message); + context.logger.error(error.message); context.sendMessage('Sorry, I can\'t think right now', context.room?.id, { label: false }, context.user.username); + } finally { + processing.delete(context.user.username); } - - processing.delete(context.user.username); } function onMessage(message, context) {