Allow aborting of API requests

This commit is contained in:
Webifi 2023-06-07 09:47:30 -05:00
parent de9a4f7f27
commit 5120cc44c8
2 changed files with 67 additions and 12 deletions

View File

@ -11,10 +11,7 @@
checkStateChange, checkStateChange,
showSetChatSettings, showSetChatSettings,
submitExitingPromptsNow, submitExitingPromptsNow,
deleteMessage deleteMessage
} from './Storage.svelte' } from './Storage.svelte'
import { getRequestSettingList, defaultModel } from './Settings.svelte' import { getRequestSettingList, defaultModel } from './Settings.svelte'
import { import {
@ -30,7 +27,7 @@
import Messages from './Messages.svelte' import Messages from './Messages.svelte'
import { prepareSummaryPrompt, restartProfile } from './Profiles.svelte' import { prepareSummaryPrompt, restartProfile } from './Profiles.svelte'
import { afterUpdate, onMount } from 'svelte' import { afterUpdate, onMount, onDestroy } from 'svelte'
import Fa from 'svelte-fa/src/fa.svelte' import Fa from 'svelte-fa/src/fa.svelte'
import { import {
faArrowUpFromBracket, faArrowUpFromBracket,
@ -38,7 +35,10 @@
faGear, faGear,
faPenToSquare, faPenToSquare,
faMicrophone, faMicrophone,
faLightbulb faLightbulb,
faCommentSlash
} from '@fortawesome/free-solid-svg-icons/index' } from '@fortawesome/free-solid-svg-icons/index'
// import { encode } from 'gpt-tokenizer' // import { encode } from 'gpt-tokenizer'
import { v4 as uuidv4 } from 'uuid' import { v4 as uuidv4 } from 'uuid'
@ -57,6 +57,8 @@
export let params = { chatId: '' } export let params = { chatId: '' }
const chatId: number = parseInt(params.chatId) const chatId: number = parseInt(params.chatId)
const controller = new AbortController()
let updating: boolean|number = false let updating: boolean|number = false
let updatingMessage: string = '' let updatingMessage: string = ''
let input: HTMLTextAreaElement let input: HTMLTextAreaElement
@ -96,6 +98,12 @@
// Make sure chat object is ready to go // Make sure chat object is ready to go
updateChatSettings(chatId) updateChatSettings(chatId)
onDestroy(async () => {
// clean up
// abort any pending requests.
controller.abort()
})
onMount(async () => { onMount(async () => {
// Focus the input on mount // Focus the input on mount
focusInput() focusInput()
@ -147,11 +155,11 @@
// Scroll to the bottom of the chat on update // Scroll to the bottom of the chat on update
const focusInput = () => { const focusInput = () => {
input.focus() input.focus()
setTimeout(() => scrollToBottom(), 0) scrollToBottom()
} }
const scrollToBottom = (instant:boolean = false) => { const scrollToBottom = (instant:boolean = false) => {
document.querySelector('body')?.scrollIntoView({ behavior: (instant ? 'instant' : 'smooth') as any, block: 'end' }) setTimeout(() => document.querySelector('body')?.scrollIntoView({ behavior: (instant ? 'instant' : 'smooth') as any, block: 'end' }), 0)
} }
// Send API request // Send API request
@ -360,25 +368,38 @@
chatResponse.setPromptTokenCount(promptTokenCount) chatResponse.setPromptTokenCount(promptTokenCount)
const signal = controller.signal
const fetchOptions = { const fetchOptions = {
method: 'POST', method: 'POST',
headers: { headers: {
Authorization: `Bearer ${$apiKeyStorage}`, Authorization: `Bearer ${$apiKeyStorage}`,
'Content-Type': 'application/json' 'Content-Type': 'application/json'
}, },
body: JSON.stringify(request) body: JSON.stringify(request),
signal
} }
if (opts.streaming) { if (opts.streaming) {
chatResponse.onFinish(() => { updating = false; updatingMessage = '' }) const abortListener = (e:Event) => {
chatResponse.updateFromError('User aborted request.')
}
// fetchEventSource doesn't seem to throw on abort, so...
signal.addEventListener('abort', abortListener)
chatResponse.onFinish(() => {
updating = false
updatingMessage = ''
signal.removeEventListener('abort', abortListener)
scrollToBottom()
})
fetchEventSource(apiBase + '/v1/chat/completions', { fetchEventSource(apiBase + '/v1/chat/completions', {
...fetchOptions, ...fetchOptions,
onmessage (ev) { onmessage (ev) {
// Remove updating indicator // Remove updating indicator
updating = 1 // hide indicator, but still signal we're updating updating = 1 // hide indicator, but still signal we're updating
updatingMessage = '' updatingMessage = ''
console.log('ev.data', ev.data)
if (!chatResponse.hasFinished()) { if (!chatResponse.hasFinished()) {
// console.log('ev.data', ev.data)
if (ev.data === '[DONE]') { if (ev.data === '[DONE]') {
// ?? anything to do when "[DONE]"? // ?? anything to do when "[DONE]"?
} else { } else {
@ -388,11 +409,16 @@
} }
} }
}, },
onclose () {
chatResponse.updateFromClose()
},
onerror (err) { onerror (err) {
console.error(err)
throw err throw err
} }
}).catch(err => { }).catch(err => {
chatResponse.updateFromError(err.message) chatResponse.updateFromError(err.message)
scrollToBottom()
}) })
} else { } else {
const response = await fetch(apiBase + '/v1/chat/completions', fetchOptions) const response = await fetch(apiBase + '/v1/chat/completions', fetchOptions)
@ -402,11 +428,13 @@
updating = false updating = false
updatingMessage = '' updatingMessage = ''
chatResponse.updateFromSyncResponse(json) chatResponse.updateFromSyncResponse(json)
scrollToBottom()
} }
} catch (e) { } catch (e) {
updating = false updating = false
updatingMessage = '' updatingMessage = ''
chatResponse.updateFromError(e.message) chatResponse.updateFromError(e.message)
scrollToBottom()
} }
return chatResponse return chatResponse
@ -601,9 +629,15 @@
<p class="control queue"> <p class="control queue">
<button title="Queue message, don't send yet" class="button is-ghost" on:click|preventDefault={addNewMessage}><span class="icon"><Fa icon={faArrowUpFromBracket} /></span></button> <button title="Queue message, don't send yet" class="button is-ghost" on:click|preventDefault={addNewMessage}><span class="icon"><Fa icon={faArrowUpFromBracket} /></span></button>
</p> </p>
{#if updating}
<p class="control send"> <p class="control send">
<button title="Send" class="button is-info" class:is-disabled={updating} type="submit"><span class="icon"><Fa icon={faPaperPlane} /></span></button> <button title="Cancel Response" class="button is-danger" type="button" on:click={() => { controller.abort() }}><span class="icon"><Fa icon={faCommentSlash} /></span></button>
</p> </p>
{:else}
<p class="control send">
<button title="Send" class="button is-info" type="submit"><span class="icon"><Fa icon={faPaperPlane} /></span></button>
</p>
{/if}
</form> </form>
<!-- a target to scroll to --> <!-- a target to scroll to -->
<div class="content has-text-centered running-total-container"> <div class="content has-text-centered running-total-container">

View File

@ -1,6 +1,6 @@
<script context="module" lang="ts"> <script context="module" lang="ts">
// TODO: Integrate API calls // TODO: Integrate API calls
import { addMessage, updateRunningTotal } from './Storage.svelte' import { addMessage, saveChatStore, updateRunningTotal } from './Storage.svelte'
import type { Chat, ChatCompletionOpts, Message, Response, Usage } from './Types.svelte' import type { Chat, ChatCompletionOpts, Message, Response, Usage } from './Types.svelte'
import { encode } from 'gpt-tokenizer' import { encode } from 'gpt-tokenizer'
import { v4 as uuidv4 } from 'uuid' import { v4 as uuidv4 } from 'uuid'
@ -85,6 +85,7 @@ export class ChatCompletionResponse {
} }
updateFromError (errorMessage: string): void { updateFromError (errorMessage: string): void {
if (this.finished) return
this.error = errorMessage this.error = errorMessage
if (this.opts.autoAddMessages) { if (this.opts.autoAddMessages) {
addMessage(this.chat.id, { addMessage(this.chat.id, {
@ -97,6 +98,10 @@ export class ChatCompletionResponse {
this.finish() this.finish()
} }
updateFromClose (): void {
setTimeout(() => this.finish(), 100) // give others a chance to signal the finish first
}
onMessageChange = (listener: (m: Message[]) => void): number => onMessageChange = (listener: (m: Message[]) => void): number =>
this.messageChangeListeners.push(listener) this.messageChangeListeners.push(listener)
@ -126,9 +131,25 @@ export class ChatCompletionResponse {
private finish = (): void => { private finish = (): void => {
if (this.finished) return if (this.finished) return
this.finished = true this.finished = true
this.messages.forEach(m => { m.streaming = false }) // make sure all are marked stopped
saveChatStore()
const message = this.messages[0] const message = this.messages[0]
if (message) { if (message) {
updateRunningTotal(this.chat.id, message.usage as any, message.model as any) updateRunningTotal(this.chat.id, message.usage as any, message.model as any)
} else {
// If no messages it's probably because of an error or user initiated abort.
// We could miss counting the cost of the prompts sent.
// To deal with this accurately, we'd need to figure out how far the request
// made it before ending, and that may not be practical or possible to do reliably.
// For now, to error on the side of caution, we'll just count the prompts we
// sent / attempted to send. This will over-count in many error cases,
// and may under-count in others.
const usage:Usage = {
prompt_tokens: this.promptTokenCount,
completion_tokens: 0, // We have no idea if there are any to count
total_tokens: this.promptTokenCount
}
updateRunningTotal(this.chat.id, usage as any, this.chat.settings.model as any)
} }
this.notifyFinish() this.notifyFinish()
if (this.error) { if (this.error) {