From 5498c64f5dc51ab62a5d78148911a3950fc9e0b4 Mon Sep 17 00:00:00 2001 From: Spicy_Marinara Date: Fri, 6 Feb 2026 16:53:24 +0100 Subject: [PATCH] Opussy bug fix --- index.js | 2 +- src/systems/features/avatarGenerator.js | 5 +- src/systems/generation/apiClient.js | 14 +-- src/systems/ui/encounterUI.js | 9 +- src/utils/responseExtractor.js | 122 ++++++++++++++++++++++++ 5 files changed, 138 insertions(+), 14 deletions(-) create mode 100644 src/utils/responseExtractor.js diff --git a/index.js b/index.js index e8f89e9..4237e5c 100644 --- a/index.js +++ b/index.js @@ -1,5 +1,5 @@ import { getContext, renderExtensionTemplateAsync, extension_settings as st_extension_settings } from '../../../extensions.js'; -import { eventSource, event_types, substituteParams, chat, generateRaw, saveSettingsDebounced, chat_metadata, saveChatDebounced, user_avatar, getThumbnailUrl, characters, this_chid, extension_prompt_types, extension_prompt_roles, setExtensionPrompt, reloadCurrentChat, Generate, getRequestHeaders } from '../../../../script.js'; +import { eventSource, event_types, substituteParams, chat, saveSettingsDebounced, chat_metadata, saveChatDebounced, user_avatar, getThumbnailUrl, characters, this_chid, extension_prompt_types, extension_prompt_roles, setExtensionPrompt, reloadCurrentChat, Generate, getRequestHeaders } from '../../../../script.js'; import { selected_group, getGroupMembers } from '../../../group-chats.js'; import { power_user } from '../../../power-user.js'; diff --git a/src/systems/features/avatarGenerator.js b/src/systems/features/avatarGenerator.js index 629bb41..092ff3f 100644 --- a/src/systems/features/avatarGenerator.js +++ b/src/systems/features/avatarGenerator.js @@ -9,7 +9,8 @@ * - Manual regeneration support */ -import { generateRaw, characters, this_chid } from '../../../../../../../script.js'; +import { characters, this_chid } from '../../../../../../../script.js'; +import { safeGenerateRaw } from '../../utils/responseExtractor.js'; import { executeSlashCommandsOnChatInput } from '../../../../../../../scripts/slash-commands.js'; import { selected_group, getGroupMembers } from '../../../../../../group-chats.js'; import { extensionSettings, sessionAvatarPrompts, setSessionAvatarPrompt } from '../../core/state.js'; @@ -254,7 +255,7 @@ async function generateAvatarPrompt(characterName) { // console.log('[RPG Avatar] Using external API for avatar prompt generation'); response = await generateWithExternalAPI(promptMessages); } else { - response = await generateRaw({ + response = await safeGenerateRaw({ prompt: promptMessages, quietToLoud: false }); diff --git a/src/systems/generation/apiClient.js b/src/systems/generation/apiClient.js index 6cb3024..4c38f68 100644 --- a/src/systems/generation/apiClient.js +++ b/src/systems/generation/apiClient.js @@ -3,8 +3,9 @@ * Handles API calls for RPG tracker generation */ -import { generateRaw, chat, eventSource } from '../../../../../../../script.js'; +import { chat, eventSource } from '../../../../../../../script.js'; import { executeSlashCommandsOnChatInput } from '../../../../../../../scripts/slash-commands.js'; +import { safeGenerateRaw, extractTextFromResponse } from '../../utils/responseExtractor.js'; // Custom event name for when RPG Companion finishes updating tracker data // Other extensions can listen for this event to know when RPG Companion is done @@ -107,11 +108,10 @@ export async function generateWithExternalAPI(messages) { const data = await response.json(); - if (!data.choices || !data.choices[0] || !data.choices[0].message) { - throw new Error('Invalid response format from external API'); + const content = extractTextFromResponse(data); + if (!content || !content.trim()) { + throw new Error('Invalid response format from external API — no text content found'); } - - const content = data.choices[0].message.content; // console.log('[RPG Companion] External API response received successfully'); return content; @@ -255,8 +255,8 @@ export async function updateRPGData(renderUserStats, renderInfoBox, renderThough // console.log('[RPG Companion] Using external API for tracker generation'); response = await generateWithExternalAPI(prompt); } else { - // Separate mode: Use SillyTavern's generateRaw - response = await generateRaw({ + // Separate mode: Use SillyTavern's generateRaw (with extended thinking fallback) + response = await safeGenerateRaw({ prompt: prompt, quietToLoud: false }); diff --git a/src/systems/ui/encounterUI.js b/src/systems/ui/encounterUI.js index e5a6760..1d582b6 100644 --- a/src/systems/ui/encounterUI.js +++ b/src/systems/ui/encounterUI.js @@ -4,7 +4,8 @@ */ import { getContext } from '../../../../../../extensions.js'; -import { generateRaw, chat, saveChatDebounced, characters, this_chid, user_avatar } from '../../../../../../../script.js'; +import { chat, saveChatDebounced, characters, this_chid, user_avatar } from '../../../../../../../script.js'; +import { safeGenerateRaw } from '../../utils/responseExtractor.js'; import { selected_group, getGroupMembers, groups } from '../../../../../../group-chats.js'; import { executeSlashCommandsOnChatInput } from '../../../../../../../scripts/slash-commands.js'; import { extensionSettings } from '../../core/state.js'; @@ -81,7 +82,7 @@ export class EncounterModal { // Store request for potential regeneration this.lastRequest = { type: 'init', prompt: initPrompt }; - const response = await generateRaw({ + const response = await safeGenerateRaw({ prompt: initPrompt, quietToLoud: false }); @@ -816,7 +817,7 @@ export class EncounterModal { // Store request for potential regeneration this.lastRequest = { type: 'action', action, prompt: actionPrompt }; - const response = await generateRaw({ + const response = await safeGenerateRaw({ prompt: actionPrompt, quietToLoud: false }); @@ -1078,7 +1079,7 @@ export class EncounterModal { // Generate summary const summaryPrompt = await buildCombatSummaryPrompt(currentEncounter.encounterLog, result); - const summaryResponse = await generateRaw({ + const summaryResponse = await safeGenerateRaw({ prompt: summaryPrompt, quietToLoud: false }); diff --git a/src/utils/responseExtractor.js b/src/utils/responseExtractor.js new file mode 100644 index 0000000..7f4308c --- /dev/null +++ b/src/utils/responseExtractor.js @@ -0,0 +1,122 @@ +/** + * Response Extractor Utility + * + * Handles extraction of text content from various API response formats. + * Fixes the "No message generated" error caused by Claude models with + * extended thinking, where the API response `content` field is an array + * of content blocks instead of a single string. + * + * Also provides a safe wrapper around SillyTavern's `generateRaw` that + * intercepts the raw fetch response as a fallback. + */ + +import { generateRaw } from '../../../../../../../script.js'; + +/** + * Extracts text from any API response shape (Anthropic content-block arrays, + * OpenAI choices, plain strings, etc.). + * + * @param {*} response - The raw API response (string, array, or object) + * @returns {string} The extracted text content + */ +export function extractTextFromResponse(response) { + if (!response) return ''; + if (typeof response === 'string') return response; + + // Response itself is an array of content blocks (Anthropic extended thinking) + if (Array.isArray(response)) { + const texts = response + .filter(b => b && b.type === 'text' && typeof b.text === 'string') + .map(b => b.text); + if (texts.length > 0) return texts.join('\n'); + + const strings = response.filter(item => typeof item === 'string'); + if (strings.length > 0) return strings.join('\n'); + + return JSON.stringify(response); + } + + // response.content (string or Anthropic content array) + if (response.content !== undefined && response.content !== null) { + if (typeof response.content === 'string') return response.content; + if (Array.isArray(response.content)) { + const texts = response.content + .filter(b => b && b.type === 'text' && typeof b.text === 'string') + .map(b => b.text); + if (texts.length > 0) return texts.join('\n'); + } + } + + // OpenAI choices format + if (response.choices?.[0]?.message?.content) { + const c = response.choices[0].message.content; + if (typeof c === 'string') return c; + if (Array.isArray(c)) { + const texts = c + .filter(b => b && b.type === 'text' && typeof b.text === 'string') + .map(b => b.text); + if (texts.length > 0) return texts.join('\n'); + } + } + + // Other common fields + if (typeof response.text === 'string') return response.text; + if (typeof response.message === 'string') return response.message; + if (response.message?.content && typeof response.message.content === 'string') { + return response.message.content; + } + + return JSON.stringify(response); +} + +/** + * Safe wrapper around SillyTavern's `generateRaw`. + * + * Temporarily intercepts `window.fetch` to capture the raw API response. + * If `generateRaw` throws "No message generated" (e.g. because the first + * content block from Claude extended thinking is empty), we extract the + * real text from the captured raw data ourselves. + * + * @param {object} options - Options passed directly to `generateRaw` + * @param {Array<{role: string, content: string}>} options.prompt - Message array + * @param {boolean} [options.quietToLoud] - Whether to use quiet-to-loud mode + * @returns {Promise} The generated text + */ +export async function safeGenerateRaw(options) { + let capturedRawData = null; + const originalFetch = window.fetch; + + window.fetch = async function (...args) { + const response = await originalFetch.apply(this, args); + try { + const url = typeof args[0] === 'string' ? args[0] : args[0]?.url || ''; + if (url.includes('/api/backends/chat-completions/generate') || + (url.includes('/api/backends/') && url.includes('/generate'))) { + const clone = response.clone(); + capturedRawData = await clone.json(); + } + } catch (e) { + /* ignore clone/parse errors */ + } + return response; + }; + + try { + const result = await generateRaw(options); + return result; + } catch (genErr) { + if (genErr.message?.includes('No message generated') && capturedRawData) { + console.warn( + '[RPG Companion] generateRaw failed (likely extended thinking). Extracting from raw API data.', + ); + const extracted = extractTextFromResponse(capturedRawData); + if (!extracted || !extracted.trim()) { + throw new Error('Could not extract text from API response'); + } + return extracted; + } + throw genErr; // Re-throw non-related errors + } finally { + window.fetch = originalFetch; // ALWAYS restore original fetch + } +}