Opussy bug fix

This commit is contained in:
Spicy_Marinara
2026-02-06 16:53:24 +01:00
parent 5fa369e3d7
commit 5498c64f5d
5 changed files with 138 additions and 14 deletions
+1 -1
View File
@@ -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';
+3 -2
View File
@@ -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
});
+7 -7
View File
@@ -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
});
+5 -4
View File
@@ -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
});
+122
View File
@@ -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<string>} 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
}
}