import { appName, appRepository, appVersion } from "./appInfo"; import { addLogEntry } from "./logEntries"; import { getSettings, getTextGenerationState, updateResponse, updateTextGenerationState, } from "./pubSub"; import { sleep } from "./sleep"; import { ChatGenerationError, canStartResponding, defaultContextSize, getDefaultChatMessages, getFormattedSearchResults, } from "./textGenerationUtilities"; import type { ChatMessage } from "./types"; /** * Response from AI Horde API */ interface HordeResponse { /** Request ID */ id: string; /** Kudos cost for the request */ kudos: number; } /** * Status response from AI Horde API */ interface HordeStatusResponse { /** Generated text results */ generations?: { text: string; model: string }[]; /** Whether generation is complete */ done?: boolean; /** Whether generation failed */ faulted?: boolean; /** Whether generation is still possible */ is_possible?: boolean; } /** * Model information from AI Horde */ interface HordeModelInfo { /** Model performance rating */ performance: number; /** Number of queued requests */ queued: number; /** Number of active jobs */ jobs: number; /** Estimated time to completion */ eta: number; /** Model type */ type: string; /** Model name */ name: string; /** Number of available instances */ count: number; } /** * User information from AI Horde */ interface HordeUserInfo { /** Username */ username: string; /** Available kudos */ kudos: number; } /** * Base URL for the public AI Horde API (browser-safe) */ const AI_HORDE_BASE_URL = "https://aihorde.net/api/v2"; /** * Client agent identifier for AI Horde API */ const aiHordeClientAgent = `${appName}:${appVersion}:${appRepository}`; /** * Marker for user messages in chat history */ const userMarker = "**USER**:"; /** * Marker for assistant messages in chat history */ const assistantMarker = "**ASSISTANT**:"; // HTTP header constants for consistency const HTTP_HEADERS = { CONTENT_TYPE: "Content-Type", CLIENT_AGENT: "Client-Agent", ACCEPT: "accept", API_KEY: "apikey", } as const; function buildHordeUrl(path: string) { return `${AI_HORDE_BASE_URL}${path}`; } export const aiHordeDefaultApiKey = "0000000000"; function getEffectiveHordeApiKey( settings: ReturnType = getSettings(), ) { return settings.hordeApiKey || aiHordeDefaultApiKey; } async function startGeneration(messages: ChatMessage[], signal?: AbortSignal) { const settings = getSettings(); const aiHordeApiKey = getEffectiveHordeApiKey(settings); const aiHordeMaxResponseLengthInTokens = aiHordeApiKey === aiHordeDefaultApiKey ? 512 : 1024; const response = await fetch(buildHordeUrl("/generate/text/async"), { method: "POST", signal, headers: { [HTTP_HEADERS.CONTENT_TYPE]: "application/json", [HTTP_HEADERS.CLIENT_AGENT]: aiHordeClientAgent, [HTTP_HEADERS.API_KEY]: aiHordeApiKey, }, body: JSON.stringify({ prompt: formatPrompt(messages), params: { max_context_length: defaultContextSize, max_length: aiHordeMaxResponseLengthInTokens, singleline: false, temperature: settings.inferenceTemperature, top_p: settings.inferenceTopP, min_p: settings.minP, top_k: 0, rep_pen: 1, stop_sequence: [userMarker, assistantMarker], validated_backends: false, }, models: settings.hordeModel ? [settings.hordeModel] : undefined, }), }); const data = (await response.json()) as HordeResponse; if (!data.id) { throw new Error("Failed to start generation"); } return data; } async function handleGenerationStatus( generationId: string, onUpdate: (text: string) => void, signal?: AbortSignal, ): Promise { const aiHordeApiKey = getEffectiveHordeApiKey(); let lastText = ""; try { let status: HordeStatusResponse; do { if (signal?.aborted) { throw new Error("Request was aborted"); } const response = await fetch( buildHordeUrl(`/generate/text/status/${generationId}`), { method: "GET", signal, headers: { [HTTP_HEADERS.CLIENT_AGENT]: aiHordeClientAgent, [HTTP_HEADERS.CONTENT_TYPE]: "application/json", [HTTP_HEADERS.API_KEY]: aiHordeApiKey, }, }, ); status = await response.json(); if ( status.generations?.[0]?.text && status.generations[0].text !== lastText ) { lastText = status.generations[0].text; if (status.generations[0].model) { addLogEntry( `AI Horde completed the generation using the model "${status.generations[0].model}"`, ); } onUpdate(lastText.split(userMarker)[0]); } if (!status.done && !status.faulted && status.is_possible) { await sleep(1000); } if (getTextGenerationState() === "interrupted") { throw new ChatGenerationError("Generation interrupted"); } } while ( !status.done && !status.faulted && status.is_possible && !signal?.aborted ); if (signal?.aborted) { throw new Error("Request was aborted"); } if (status.faulted) { throw new ChatGenerationError("Generation failed"); } if (!status.is_possible) { throw new ChatGenerationError( "Generation not possible with the selected model", ); } const generatedText = status.generations?.[0].text; if (!generatedText) { throw new Error("No text generated"); } return generatedText.split(userMarker)[0]; } catch (error) { if (signal?.aborted) { throw new Error("Request was aborted"); } if (error instanceof ChatGenerationError) { throw error; } throw new Error(`Error while checking generation status: ${error}`); } } async function cancelGeneration(generationId: string): Promise { const aiHordeApiKey = getEffectiveHordeApiKey(); try { const response = await fetch( buildHordeUrl(`/generate/text/status/${generationId}`), { method: "DELETE", headers: { [HTTP_HEADERS.CLIENT_AGENT]: aiHordeClientAgent, [HTTP_HEADERS.CONTENT_TYPE]: "application/json", [HTTP_HEADERS.API_KEY]: aiHordeApiKey, }, }, ); if (!response.ok) { throw new Error( `Cancel request failed: ${response.status} ${response.statusText}`, ); } addLogEntry(`Successfully cancelled generation ${generationId}`); } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); addLogEntry(`Failed to cancel generation ${generationId}: ${errorMessage}`); throw new Error(`Failed to cancel generation: ${errorMessage}`); } } export async function fetchHordeModels(): Promise { const response = await fetch( buildHordeUrl("/status/models?type=text&model_state=all"), { method: "GET", headers: { [HTTP_HEADERS.CLIENT_AGENT]: aiHordeClientAgent, [HTTP_HEADERS.ACCEPT]: "application/json", }, }, ); if (!response.ok) { throw new Error("Failed to fetch AI Horde models"); } return response.json(); } export async function fetchHordeUserInfo( apiKey: string, ): Promise { const response = await fetch(buildHordeUrl("/find_user"), { headers: { [HTTP_HEADERS.API_KEY]: apiKey, [HTTP_HEADERS.CLIENT_AGENT]: aiHordeClientAgent, }, }); if (!response.ok) { throw new Error(`Failed to fetch user info: ${response.statusText}`); } const data = await response.json(); return { username: data.username, kudos: data.kudos, }; } export async function generateTextWithHorde() { await canStartResponding(); updateTextGenerationState("preparingToGenerate"); const messages = getDefaultChatMessages(getFormattedSearchResults(true)); await executeHordeGeneration(messages, (text) => { updateResponse(text); }); } export async function generateChatWithHorde( messages: ChatMessage[], onUpdate: (partialResponse: string) => void, ) { return await executeHordeGeneration(messages, onUpdate); } async function executeHordeGeneration( messages: ChatMessage[], onUpdate: (text: string) => void, ): Promise { const settings = getSettings(); if (settings.hordeModel) { const generation = await startGeneration(messages); return await handleGenerationStatus(generation.id, onUpdate); } const controllers: AbortController[] = [ new AbortController(), new AbortController(), ]; try { const startPromises = controllers.map(async (ctrl) => { const generation = await startGeneration(messages, ctrl.signal); return { id: generation.id, ctrl }; }); const startResults = await Promise.allSettled(startPromises); const generations: Array<{ id: string; ctrl: AbortController }> = startResults .map((generationPromise) => generationPromise.status === "fulfilled" ? generationPromise.value : null, ) .filter( (generation): generation is { id: string; ctrl: AbortController } => generation !== null, ); if (generations.length === 0) { throw new Error("Failed to start any AI Horde generation"); } const raceState = { winnerId: null as string | null, hasWinner: false }; const statusPromises = generations.map((generation) => handleGenerationStatus( generation.id, (text: string) => { // Atomic check and set to prevent race conditions if (!raceState.hasWinner) { raceState.winnerId = generation.id; raceState.hasWinner = true; } // Only update if this generation is the winner if (raceState.winnerId === generation.id) { onUpdate(text); } }, generation.ctrl.signal, ).then((result: string) => ({ result, generationId: generation.id })), ); const winner = await Promise.race(statusPromises); await Promise.all( generations.map(async (generation) => { if (generation.id !== winner.generationId) { try { generation.ctrl.abort(); } catch (abortError) { addLogEntry( `Failed to abort generation ${generation.id}: ${abortError}`, ); } try { await cancelGeneration(generation.id); } catch (cancelError) { addLogEntry( `Failed to cancel generation ${generation.id}: ${cancelError}`, ); } } }), ); return winner.result; } catch (error) { controllers.forEach((controller) => { try { controller.abort(); } catch (abortError) { addLogEntry(`Failed to abort controller: ${abortError}`); } }); throw error; } } function formatPrompt(messages: ChatMessage[]): string { return `${messages .map((msg) => `**${msg.role?.toUpperCase()}**:\n${msg.content}`) .join("\n\n")}\n\n${assistantMarker}\n`; }