Allow aborting of API requests
This commit is contained in:
parent
de9a4f7f27
commit
5120cc44c8
|
@ -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">
|
||||||
|
|
|
@ -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) {
|
||||||
|
|
Loading…
Reference in New Issue