From 9dfdef46d565483065593f9d96a75ea020a0e880 Mon Sep 17 00:00:00 2001 From: Luis Date: Mon, 9 Mar 2026 13:42:59 -0400 Subject: [PATCH] Add z.ai fallback provider for AI responses - Try NanoGPT first, fall back to z.ai GLM-4.7-Flash on failure - Add ZAI_API_KEY environment variable - Update deploy workflow with new secret --- .gitea/workflows/deploy-nas.yml | 1 + utils/ai.js | 142 +++++++++++++++++++++++--------- 2 files changed, 102 insertions(+), 41 deletions(-) diff --git a/.gitea/workflows/deploy-nas.yml b/.gitea/workflows/deploy-nas.yml index 77715f7..ae676b1 100644 --- a/.gitea/workflows/deploy-nas.yml +++ b/.gitea/workflows/deploy-nas.yml @@ -42,6 +42,7 @@ jobs: DISCORD_TOKEN=${{ secrets.DISCORD_TOKEN }} CLIENT_ID=${{ secrets.DISCORD_CLIENT_ID }} NANOGPT_API_KEY=${{ secrets.NANOGPT_API_KEY }} + ZAI_API_KEY=${{ secrets.ZAI_API_KEY }} NANO_MODEL=${{ secrets.NANO_MODEL }} NODE_ENV=production EOF diff --git a/utils/ai.js b/utils/ai.js index 96a0fd7..aa4ddc3 100644 --- a/utils/ai.js +++ b/utils/ai.js @@ -1,9 +1,11 @@ -// ai.js - NanoGPT API wrapper for kekbot +// ai.js - NanoGPT API wrapper for kekbot with fallback support // Copyright (C) 2025 Luis Bauza import axios from 'axios'; const DEFAULT_MODEL = 'GLM-4.5-Air-Derestricted-Steam-ReExtract'; +const FALLBACK_MODEL = 'GLM-4.7-Flash'; +const FALLBACK_API_URL = 'https://api.z.ai/api/coding/paas/v4/chat/completions'; const SASSY_SYSTEM_PROMPT = `You are kekbot, a sassy Discord chatter with a big personality. @@ -41,12 +43,14 @@ CONTEXT: You're chatting in a Discord server. Multiple people might be talking. const conversationHistory = new Map(); const MAX_HISTORY_PER_CHANNEL = 10; -export async function getAIResponse(prompt, channelId, mentionedUsername) { - const history = conversationHistory.get(channelId) || []; +// Estimate token count (rough approximation: ~4 chars per token) +function estimateTokens(text) { + return Math.ceil(text.length / 4); +} - // Build messages array with system prompt and history +function buildMessages(history, prompt, mentionedUsername, systemPrompt) { const messages = [ - { role: 'system', content: SASSY_SYSTEM_PROMPT }, + { role: 'system', content: systemPrompt }, ...history, { role: 'user', @@ -55,48 +59,104 @@ export async function getAIResponse(prompt, channelId, mentionedUsername) { : prompt } ]; + return messages; +} +async function callNanoGPT(messages, model) { + const response = await axios.post( + 'https://nano-gpt.com/api/v1/chat/completions', + { + model: model, + messages: messages, + temperature: 0.8, + max_tokens: 500, + }, + { + headers: { + Authorization: `Bearer ${process.env.NANOGPT_API_KEY}`, + 'Content-Type': 'application/json', + }, + timeout: 60000, + } + ); + return response.data.choices[0].message.content; +} + +async function callFallbackAPI(messages) { + const response = await axios.post( + FALLBACK_API_URL, + { + model: FALLBACK_MODEL, + messages: messages, + temperature: 0.8, + max_tokens: 500, + }, + { + headers: { + Authorization: `Bearer ${process.env.ZAI_API_KEY}`, + 'Content-Type': 'application/json', + }, + timeout: 60000, + } + ); + return response.data.choices[0].message.content; +} + +export async function getAIResponse(prompt, channelId, mentionedUsername) { + const history = conversationHistory.get(channelId) || []; + const messages = buildMessages(history, prompt, mentionedUsername, SASSY_SYSTEM_PROMPT); const model = process.env.NANO_MODEL || DEFAULT_MODEL; - try { - const response = await axios.post( - 'https://nano-gpt.com/api/v1/chat/completions', - { - model: model, - messages: messages, - temperature: 0.8, - max_tokens: 500, - }, - { - headers: { - Authorization: `Bearer ${process.env.NANOGPT_API_KEY}`, - 'Content-Type': 'application/json', - }, - timeout: 60000, + // Try NanoGPT first + if (process.env.NANOGPT_API_KEY) { + try { + console.log('Attempting NanoGPT...'); + const aiResponse = await callNanoGPT(messages, model); + + // Update history on success + const newHistory = [ + ...history, + { role: 'user', content: prompt }, + { role: 'assistant', content: aiResponse } + ]; + + if (newHistory.length > MAX_HISTORY_PER_CHANNEL) { + newHistory.splice(0, newHistory.length - MAX_HISTORY_PER_CHANNEL); } - ); + conversationHistory.set(channelId, newHistory); - const aiResponse = response.data.choices[0].message.content; - - // Add to conversation history - const newHistory = [ - ...history, - { role: 'user', content: prompt }, - { role: 'assistant', content: aiResponse } - ]; - - // Trim history if too long - if (newHistory.length > MAX_HISTORY_PER_CHANNEL) { - newHistory.splice(0, newHistory.length - MAX_HISTORY_PER_CHANNEL); + return aiResponse; + } catch (error) { + console.error('NanoGPT failed:', error.response?.data || error.message); } - - conversationHistory.set(channelId, newHistory); - - return aiResponse; - } catch (error) { - console.error('NanoGPT API error:', error.response?.data || error.message); - throw error; } + + // Fallback to z.ai if NanoGPT failed or not configured + if (process.env.ZAI_API_KEY) { + try { + console.log('Attempting fallback (z.ai)...'); + const aiResponse = await callFallbackAPI(messages); + + // Update history on success + const newHistory = [ + ...history, + { role: 'user', content: prompt }, + { role: 'assistant', content: aiResponse } + ]; + + if (newHistory.length > MAX_HISTORY_PER_CHANNEL) { + newHistory.splice(0, newHistory.length - MAX_HISTORY_PER_CHANNEL); + } + conversationHistory.set(channelId, newHistory); + + return aiResponse; + } catch (fallbackError) { + console.error('Fallback API also failed:', fallbackError.response?.data || fallbackError.message); + throw new Error('All AI providers failed'); + } + } + + throw new Error('No AI provider configured'); } export function clearHistory(channelId) { @@ -104,5 +164,5 @@ export function clearHistory(channelId) { } export function isAIEnabled() { - return !!process.env.NANOGPT_API_KEY; + return !!process.env.NANOGPT_API_KEY || !!process.env.ZAI_API_KEY; }