Merge pull request #235 from Webifi/main

Experimental support for Petals/Llama 2
This commit is contained in:
Niek van der Maas 2023-07-25 07:32:18 +02:00 committed by GitHub
commit f877ac09ab
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
22 changed files with 854 additions and 218 deletions

7
package-lock.json generated
View File

@ -27,6 +27,7 @@
"eslint-plugin-svelte3": "^4.0.0",
"flourite": "^1.2.4",
"gpt-tokenizer": "^2.0.0",
"llama-tokenizer-js": "^1.1.1",
"postcss": "^8.4.26",
"sass": "^1.63.6",
"stacking-order": "^2.0.0",
@ -3182,6 +3183,12 @@
"node": ">= 0.8.0"
}
},
"node_modules/llama-tokenizer-js": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/llama-tokenizer-js/-/llama-tokenizer-js-1.1.1.tgz",
"integrity": "sha512-5H2oSJnSufWGhOw6hcCGAqJeB3POmeIBzRklH3cXs0L4MSAYdwoYTodni4j5YVo6jApdhaqaNVU66gNRgXeBRg==",
"dev": true
},
"node_modules/locate-path": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz",

View File

@ -33,6 +33,7 @@
"eslint-plugin-svelte3": "^4.0.0",
"flourite": "^1.2.4",
"gpt-tokenizer": "^2.0.0",
"llama-tokenizer-js": "^1.1.1",
"postcss": "^8.4.26",
"sass": "^1.63.6",
"stacking-order": "^2.0.0",

View File

@ -5,10 +5,12 @@
const endpointGenerations = import.meta.env.VITE_ENDPOINT_GENERATIONS || '/v1/images/generations'
const endpointModels = import.meta.env.VITE_ENDPOINT_MODELS || '/v1/models'
const endpointEmbeddings = import.meta.env.VITE_ENDPOINT_EMBEDDINGS || '/v1/embeddings'
const endpointPetals = import.meta.env.VITE_PEDALS_WEBSOCKET || 'wss://chat.petals.dev/api/v2/generate'
export const getApiBase = ():string => apiBase
export const getEndpointCompletions = ():string => endpointCompletions
export const getEndpointGenerations = ():string => endpointGenerations
export const getEndpointModels = ():string => endpointModels
export const getEndpointEmbeddings = ():string => endpointEmbeddings
export const getPetals = ():string => endpointPetals
</script>

View File

@ -40,6 +40,7 @@
import { openModal } from 'svelte-modals'
import PromptInput from './PromptInput.svelte'
import { ChatRequest } from './ChatRequest.svelte'
import { getModelDetail } from './Models.svelte'
export let params = { chatId: '' }
const chatId: number = parseInt(params.chatId)
@ -245,6 +246,19 @@
chatRequest.updating = true
chatRequest.updatingMessage = ''
let doScroll = true
let didScroll = false
const checkUserScroll = (e: Event) => {
const el = e.target as HTMLElement
if (el && e.isTrusted && didScroll) {
// from user
doScroll = (window.innerHeight + window.scrollY + 10) >= document.body.offsetHeight
}
}
window.addEventListener('scroll', checkUserScroll)
try {
const response = await chatRequest.sendRequest($currentChatMessages, {
chat,
@ -252,7 +266,8 @@
streaming: chatSettings.stream,
fillMessage,
onMessageChange: (messages) => {
scrollToBottom(true)
if (doScroll) scrollToBottom(true)
didScroll = !!messages[0]?.content
}
})
await response.promiseToFinish()
@ -264,6 +279,8 @@
console.error(e)
}
window.removeEventListener('scroll', checkUserScroll)
chatRequest.updating = false
chatRequest.updatingMessage = ''
@ -273,13 +290,16 @@
const suggestName = async (): Promise<void> => {
const suggestMessage: Message = {
role: 'user',
content: "Using appropriate language, please give a 5 word summary of this conversation's topic.",
content: "Using appropriate language, please tell me a short 6 word summary of this conversation's topic for use as a book title. Only respond with the summary.",
uuid: uuidv4()
}
const suggestMessages = $currentChatMessages.slice(0, 10) // limit to first 10 messages
suggestMessages.push(suggestMessage)
chatRequest.updating = true
chatRequest.updatingMessage = 'Getting suggestion for chat name...'
const response = await chatRequest.sendRequest(suggestMessages, {
chat,
autoAddMessages: false,
@ -297,7 +317,7 @@
})
} else {
response.getMessages().forEach(m => {
const name = m.content.split(/\s+/).slice(0, 8).join(' ').trim()
const name = m.content.split(/\s+/).slice(0, 8).join(' ').replace(/^[^a-z0-9!?]+|[^a-z0-9!?]+$/gi, '').trim()
if (name) chat.name = name
})
saveChatStore()
@ -420,7 +440,7 @@
<div class="content has-text-centered running-total-container">
{#each Object.entries(chat.usage || {}) as [model, usage]}
<p class="is-size-7 running-totals">
<em>{model}</em> total <span class="has-text-weight-bold">{usage.total_tokens}</span>
<em>{getModelDetail(model || '').label || model}</em> total <span class="has-text-weight-bold">{usage.total_tokens}</span>
tokens ~= <span class="has-text-weight-bold">${getPrice(usage, model).toFixed(6)}</span>
</p>
{/each}

View File

@ -1,9 +1,9 @@
<script context="module" lang="ts">
import { setImage } from './ImageStore.svelte'
import { countTokens } from './Models.svelte'
// TODO: Integrate API calls
import { addMessage, getLatestKnownModel, saveChatStore, setLatestKnownModel, subtractRunningTotal, updateRunningTotal } from './Storage.svelte'
import { addMessage, getLatestKnownModel, setLatestKnownModel, subtractRunningTotal, updateMessages, updateRunningTotal } from './Storage.svelte'
import type { Chat, ChatCompletionOpts, ChatImage, Message, Model, Response, ResponseImage, Usage } from './Types.svelte'
import { encode } from 'gpt-tokenizer'
import { v4 as uuidv4 } from 'uuid'
export class ChatCompletionResponse {
@ -65,6 +65,10 @@ export class ChatCompletionResponse {
this.promptTokenCount = tokens
}
getPromptTokenCount (): number {
return this.promptTokenCount
}
async updateImageFromSyncResponse (response: ResponseImage, prompt: string, model: Model) {
this.setModel(model)
for (let i = 0; i < response.data.length; i++) {
@ -138,10 +142,10 @@ export class ChatCompletionResponse {
message.content = this.initialFillMerge(message.content, choice.delta?.content)
message.content += choice.delta.content
}
completionTokenCount += encode(message.content).length
completionTokenCount += countTokens(this.model, message.content)
message.model = response.model
message.finish_reason = choice.finish_reason
message.streaming = choice.finish_reason === null && !this.finished
message.streaming = !choice.finish_reason && !this.finished
this.messages[i] = message
})
// total up the tokens
@ -171,15 +175,15 @@ export class ChatCompletionResponse {
} as Message)
}
this.notifyMessageChange()
setTimeout(() => this.finish(), 250) // give others a chance to signal the finish first
setTimeout(() => this.finish(), 200) // give others a chance to signal the finish first
}
updateFromClose (force: boolean = false): void {
if (!this.finished && !this.error && !this.messages?.find(m => m.content)) {
if (!force) return setTimeout(() => this.updateFromClose(true), 250) as any
return this.updateFromError('Unexpected connection termination')
if (!force) return setTimeout(() => this.updateFromClose(true), 300) as any
if (!this.finished) return this.updateFromError('Unexpected connection termination')
}
setTimeout(() => this.finish(), 250) // give others a chance to signal the finish first
setTimeout(() => this.finish(), 260) // give others a chance to signal the finish first
}
onMessageChange = (listener: (m: Message[]) => void): number =>
@ -209,10 +213,10 @@ export class ChatCompletionResponse {
}
private finish = (): void => {
this.messages.forEach(m => { m.streaming = false }) // make sure all are marked stopped
updateMessages(this.chat.id)
if (this.finished) return
this.finished = true
this.messages.forEach(m => { m.streaming = false }) // make sure all are marked stopped
saveChatStore()
const message = this.messages[0]
const model = this.model || getLatestKnownModel(this.chat.settings.model)
if (message) {

View File

@ -1,7 +1,7 @@
<script lang="ts">
import { replace } from 'svelte-spa-router'
import type { Chat } from './Types.svelte'
import { apiKeyStorage, deleteChat, pinMainMenu, saveChatStore } from './Storage.svelte'
import { deleteChat, hasActiveModels, pinMainMenu, saveChatStore } from './Storage.svelte'
import Fa from 'svelte-fa/src/fa.svelte'
import { faTrash, faCircleCheck, faPencil } from '@fortawesome/free-solid-svg-icons/index'
import { faMessage } from '@fortawesome/free-regular-svg-icons/index'
@ -86,7 +86,7 @@
<a
href={`#/chat/${chat.id}`}
class="chat-menu-item"
class:is-waiting={waitingForConfirm} class:is-disabled={!$apiKeyStorage} class:is-active={activeChatId === chat.id}
class:is-waiting={waitingForConfirm} class:is-disabled={!hasActiveModels()} class:is-active={activeChatId === chat.id}
on:click={() => { $pinMainMenu = false }} >
{#if waitingForConfirm}
<a class="is-pulled-right is-hidden px-1 py-0 has-text-weight-bold delete-button" href={'$'} on:click|preventDefault={() => delChat()}><Fa icon={faCircleCheck} /></a>

View File

@ -18,7 +18,7 @@
faEyeSlash
} from '@fortawesome/free-solid-svg-icons/index'
import { faSquareMinus, faSquarePlus as faSquarePlusOutline } from '@fortawesome/free-regular-svg-icons/index'
import { apiKeyStorage, addChatFromJSON, chatsStorage, checkStateChange, clearChats, clearMessages, copyChat, globalStorage, setGlobalSettingValueByKey, showSetChatSettings, pinMainMenu, getChat, deleteChat, saveChatStore, saveCustomProfile } from './Storage.svelte'
import { addChatFromJSON, chatsStorage, checkStateChange, clearChats, clearMessages, copyChat, globalStorage, setGlobalSettingValueByKey, showSetChatSettings, pinMainMenu, getChat, deleteChat, saveChatStore, saveCustomProfile, hasActiveModels } from './Storage.svelte'
import { exportAsMarkdown, exportChatAsJSON } from './Export.svelte'
import { newNameForProfile, restartProfile } from './Profiles.svelte'
import { replace } from 'svelte-spa-router'
@ -173,7 +173,7 @@
<span class="menu-icon"><Fa icon={faGear}/></span> Chat Profile Settings
</a>
<hr class="dropdown-divider">
<a href={'#'} class:is-disabled={!$apiKeyStorage} on:click|preventDefault={() => { $apiKeyStorage && close(); $apiKeyStorage && startNewChatWithWarning(chatId) }} class="dropdown-item">
<a href={'#'} class:is-disabled={!hasActiveModels()} on:click|preventDefault={() => { hasActiveModels() && close(); hasActiveModels() && startNewChatWithWarning(chatId) }} class="dropdown-item">
<span class="menu-icon"><Fa icon={faSquarePlus}/></span> New Chat from Default
</a>
<a href={'#'} class:is-disabled={!chatId} on:click|preventDefault={() => { chatId && close(); chatId && startNewChatFromChatId(chatId) }} class="dropdown-item">
@ -196,14 +196,14 @@
<a href={'#'} class="dropdown-item" class:is-disabled={!chatId} on:click|preventDefault={() => { close(); exportChatAsJSON(chatId) }}>
<span class="menu-icon"><Fa icon={faDownload}/></span> Backup Chat JSON
</a>
<a href={'#'} class="dropdown-item" class:is-disabled={!$apiKeyStorage} on:click|preventDefault={() => { if (chatId) close(); chatFileInput.click() }}>
<a href={'#'} class="dropdown-item" class:is-disabled={!hasActiveModels()} on:click|preventDefault={() => { if (chatId) close(); chatFileInput.click() }}>
<span class="menu-icon"><Fa icon={faUpload}/></span> Restore Chat JSON
</a>
<a href={'#'} class="dropdown-item" class:is-disabled={!chatId} on:click|preventDefault={() => { if (chatId) close(); exportAsMarkdown(chatId) }}>
<span class="menu-icon"><Fa icon={faFileExport}/></span> Export Chat Markdown
</a>
<hr class="dropdown-divider">
<a href={'#'} class="dropdown-item" class:is-disabled={!$apiKeyStorage} on:click|preventDefault={() => { if (chatId) close(); profileFileInput.click() }}>
<a href={'#'} class="dropdown-item" class:is-disabled={!hasActiveModels()} on:click|preventDefault={() => { if (chatId) close(); profileFileInput.click() }}>
<span class="menu-icon"><Fa icon={faUpload}/></span> Restore Profile JSON
</a>
<hr class="dropdown-divider">

View File

@ -5,11 +5,12 @@
import type { Chat, ChatCompletionOpts, ChatSettings, Message, Model, Request, RequestImageGeneration } from './Types.svelte'
import { deleteMessage, getChatSettingValueNullDefault, insertMessages, getApiKey, addError, currentChatMessages, getMessages, updateMessages, deleteSummaryMessage } from './Storage.svelte'
import { scrollToBottom, scrollToMessage } from './Util.svelte'
import { getRequestSettingList, defaultModel } from './Settings.svelte'
import { EventStreamContentType, fetchEventSource } from '@microsoft/fetch-event-source'
import { getApiBase, getEndpointCompletions, getEndpointGenerations } from './ApiUtil.svelte'
import { getDefaultModel, getRequestSettingList } from './Settings.svelte'
import { v4 as uuidv4 } from 'uuid'
import { get } from 'svelte/store'
import { getEndpoint, getModelDetail } from './Models.svelte'
import { runOpenAiCompletionRequest } from './ChatRequestOpenAi.svelte'
import { runPetalsCompletionRequest } from './ChatRequestPetals.svelte'
export class ChatRequest {
constructor () {
@ -25,6 +26,15 @@ export class ChatRequest {
setChat (chat: Chat) {
this.chat = chat
this.chat.settings.model = this.getModel()
}
getChat (): Chat {
return this.chat
}
getChatSettings (): ChatSettings {
return this.chat.settings
}
// Common error handler
@ -77,7 +87,7 @@ export class ChatRequest {
const chatResponse = new ChatCompletionResponse(opts)
try {
const response = await fetch(getApiBase() + getEndpointGenerations(), fetchOptions)
const response = await fetch(getEndpoint('dall-e-' + size), fetchOptions)
if (!response.ok) {
await _this.handleError(response)
} else {
@ -159,6 +169,8 @@ export class ChatRequest {
const spl = chatSettings.sendSystemPromptLast
const sp = messagePayload[0]
if (sp) {
const lastSp = sp.content.split('::END-PROMPT::')
sp.content = lastSp[0].trim()
if (messagePayload.length > 1) {
sp.content = sp.content.replace(/::STARTUP::[\s\S]*::EOM::/, '::EOM::')
sp.content = sp.content.replace(/::STARTUP::[\s\S]*::START-PROMPT::/, '::START-PROMPT::')
@ -170,7 +182,7 @@ export class ChatRequest {
if (spl) {
messagePayload.shift()
if (messagePayload[messagePayload.length - 1]?.role === 'user') {
messagePayload.splice(-2, 0, sp)
messagePayload.splice(-1, 0, sp)
} else {
messagePayload.push(sp)
}
@ -196,11 +208,15 @@ export class ChatRequest {
}).filter(m => m.content.length)
messagePayload.splice(spl ? 0 : 1, 0, ...ms.concat(splitSystem.map(s => ({ role: 'system', content: s.trim() } as Message)).filter(m => m.content.length)))
}
const lastSpC = lastSp[1]?.trim() || ''
if (lastSpC.length) {
messagePayload.push({ role: 'system', content: lastSpC } as Message)
}
}
}
// Get token counts
const promptTokenCount = countPromptTokens(messagePayload, model)
const promptTokenCount = countPromptTokens(messagePayload, model, chat)
const maxAllowed = maxTokens - (promptTokenCount + 1)
// Build the API request body
@ -239,6 +255,9 @@ export class ChatRequest {
// Set-up and make the request
const chatResponse = new ChatCompletionResponse(opts)
const modelDetail = getModelDetail(model)
try {
// Add out token count to the response handler
// (streaming doesn't return counts, so we need to do it client side)
@ -248,88 +267,11 @@ export class ChatRequest {
// so we deal with it ourselves
_this.controller = new AbortController()
const signal = _this.controller.signal
const abortListener = (e:Event) => {
_this.updating = false
_this.updatingMessage = ''
chatResponse.updateFromError('User aborted request.')
signal.removeEventListener('abort', abortListener)
}
signal.addEventListener('abort', abortListener)
const fetchOptions = {
method: 'POST',
headers: {
Authorization: `Bearer ${getApiKey()}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(request),
signal
}
if (opts.streaming) {
/**
* Streaming request/response
* We'll get the response a token at a time, as soon as they are ready
*/
chatResponse.onFinish(() => {
_this.updating = false
_this.updatingMessage = ''
})
fetchEventSource(getApiBase() + getEndpointCompletions(), {
...fetchOptions,
openWhenHidden: true,
onmessage (ev) {
// Remove updating indicator
_this.updating = 1 // hide indicator, but still signal we're updating
_this.updatingMessage = ''
// console.log('ev.data', ev.data)
if (!chatResponse.hasFinished()) {
if (ev.data === '[DONE]') {
// ?? anything to do when "[DONE]"?
} else {
const data = JSON.parse(ev.data)
// console.log('data', data)
window.setTimeout(() => { chatResponse.updateFromAsyncResponse(data) }, 1)
}
}
},
onclose () {
_this.updating = false
_this.updatingMessage = ''
chatResponse.updateFromClose()
},
onerror (err) {
console.error(err)
throw err
},
async onopen (response) {
if (response.ok && response.headers.get('content-type') === EventStreamContentType) {
// everything's good
} else {
// client-side errors are usually non-retriable:
await _this.handleError(response)
}
}
}).catch(err => {
_this.updating = false
_this.updatingMessage = ''
chatResponse.updateFromError(err.message)
})
if (modelDetail.type === 'Petals') {
await runPetalsCompletionRequest(request, _this as any, chatResponse as any, signal, opts)
} else {
/**
* Non-streaming request/response
* We'll get the response all at once, after a long delay
*/
const response = await fetch(getApiBase() + getEndpointCompletions(), fetchOptions)
if (!response.ok) {
await _this.handleError(response)
} else {
const json = await response.json()
// Remove updating indicator
_this.updating = false
_this.updatingMessage = ''
chatResponse.updateFromSyncResponse(json)
}
await runOpenAiCompletionRequest(request, _this as any, chatResponse as any, signal, opts)
}
} catch (e) {
// console.error(e)
@ -341,12 +283,13 @@ export class ChatRequest {
return chatResponse
}
private getModel (): Model {
return this.chat.settings.model || defaultModel
getModel (): Model {
return this.chat.settings.model || getDefaultModel()
}
private buildHiddenPromptPrefixMessages (messages: Message[], insert:boolean = false): Message[] {
const chatSettings = this.chat.settings
const chat = this.chat
const chatSettings = chat.settings
const hiddenPromptPrefix = mergeProfileFields(chatSettings, chatSettings.hiddenPromptPrefix).trim()
const lastMessage = messages[messages.length - 1]
const isContinue = lastMessage?.role === 'assistant' && lastMessage.finish_reason === 'length'
@ -356,9 +299,9 @@ export class ChatRequest {
const results = hiddenPromptPrefix.split(/[\s\r\n]*::EOM::[\s\r\n]*/).reduce((a, m) => {
m = m.trim()
if (m.length) {
if (m.match(/[[USER_PROMPT]]/)) {
if (m.match(/\[\[USER_PROMPT\]\]/)) {
injectedPrompt = true
m.replace(/[[USER_PROMPT]]/g, lastMessage.content)
m = m.replace(/\[\[USER_PROMPT\]\]/g, lastMessage.content)
}
a.push({ role: a.length % 2 === 0 ? 'user' : 'assistant', content: m } as Message)
}
@ -377,7 +320,7 @@ export class ChatRequest {
lastMessage.skipOnce = true
}
}
if (injectedPrompt) results.pop()
if (injectedPrompt) messages.pop()
return results
}
return []
@ -387,11 +330,11 @@ export class ChatRequest {
* Gets an estimate of how many extra tokens will be added that won't be part of the visible messages
* @param filtered
*/
private getTokenCountPadding (filtered: Message[]): number {
private getTokenCountPadding (filtered: Message[], chat: Chat): number {
let result = 0
// add cost of hiddenPromptPrefix
result += this.buildHiddenPromptPrefixMessages(filtered)
.reduce((a, m) => a + countMessageTokens(m, this.getModel()), 0)
.reduce((a, m) => a + countMessageTokens(m, this.getModel(), chat), 0)
// more here eventually?
return result
}
@ -413,10 +356,10 @@ export class ChatRequest {
}
// Get extra counts for when the prompts are finally sent.
const countPadding = this.getTokenCountPadding(filtered)
const countPadding = this.getTokenCountPadding(filtered, chat)
// See if we have enough to apply any of the reduction modes
const fullPromptSize = countPromptTokens(filtered, model) + countPadding
const fullPromptSize = countPromptTokens(filtered, model, chat) + countPadding
if (fullPromptSize < chatSettings.summaryThreshold) return await continueRequest() // nothing to do yet
const overMax = fullPromptSize > maxTokens * 0.95
@ -439,12 +382,12 @@ export class ChatRequest {
* *************************************************************
*/
let promptSize = countPromptTokens(top.concat(rw), model) + countPadding
let promptSize = countPromptTokens(top.concat(rw), model, chat) + countPadding
while (rw.length && rw.length > pinBottom && promptSize >= chatSettings.summaryThreshold) {
const rolled = rw.shift()
// Hide messages we're "rolling"
if (rolled) rolled.suppress = true
promptSize = countPromptTokens(top.concat(rw), model) + countPadding
promptSize = countPromptTokens(top.concat(rw), model, chat) + countPadding
}
// Run a new request, now with the rolled messages hidden
return await _this.sendRequest(get(currentChatMessages), {
@ -460,26 +403,26 @@ export class ChatRequest {
const bottom = rw.slice(0 - pinBottom)
let continueCounter = chatSettings.summaryExtend + 1
rw = rw.slice(0, 0 - pinBottom)
let reductionPoolSize = countPromptTokens(rw, model)
let reductionPoolSize = countPromptTokens(rw, model, chat)
const ss = Math.abs(chatSettings.summarySize)
const getSS = ():number => (ss < 1 && ss > 0)
? Math.round(reductionPoolSize * ss) // If summarySize between 0 and 1, use percentage of reduced
: Math.min(ss, reductionPoolSize * 0.5) // If > 1, use token count
const topSize = countPromptTokens(top, model)
const topSize = countPromptTokens(top, model, chat)
let maxSummaryTokens = getSS()
let promptSummary = prepareSummaryPrompt(chatId, maxSummaryTokens)
const summaryRequest = { role: 'user', content: promptSummary } as Message
let promptSummarySize = countMessageTokens(summaryRequest, model)
let promptSummarySize = countMessageTokens(summaryRequest, model, chat)
// Make sure there is enough room to generate the summary, and try to make sure
// the last prompt is a user prompt as that seems to work better for summaries
while ((topSize + reductionPoolSize + promptSummarySize + maxSummaryTokens) >= maxTokens ||
(reductionPoolSize >= 100 && rw[rw.length - 1]?.role !== 'user')) {
bottom.unshift(rw.pop() as Message)
reductionPoolSize = countPromptTokens(rw, model)
reductionPoolSize = countPromptTokens(rw, model, chat)
maxSummaryTokens = getSS()
promptSummary = prepareSummaryPrompt(chatId, maxSummaryTokens)
summaryRequest.content = promptSummary
promptSummarySize = countMessageTokens(summaryRequest, model)
promptSummarySize = countMessageTokens(summaryRequest, model, chat)
}
if (reductionPoolSize < 50) {
if (overMax) addError(chatId, 'Check summary settings. Unable to summarize enough messages.')
@ -565,10 +508,10 @@ export class ChatRequest {
// Try to get more of it
delete summaryResponse.finish_reason
_this.updatingMessage = 'Summarizing more...'
let _recount = countPromptTokens(top.concat(rw).concat([summaryRequest]).concat([summaryResponse]), model)
let _recount = countPromptTokens(top.concat(rw).concat([summaryRequest]).concat([summaryResponse]), model, chat)
while (rw.length && (_recount + maxSummaryTokens >= maxTokens)) {
rw.shift()
_recount = countPromptTokens(top.concat(rw).concat([summaryRequest]).concat([summaryResponse]), model)
_recount = countPromptTokens(top.concat(rw).concat([summaryRequest]).concat([summaryResponse]), model, chat)
}
loopCount++
continue

View File

@ -0,0 +1,100 @@
<script context="module" lang="ts">
import { EventStreamContentType, fetchEventSource } from '@microsoft/fetch-event-source'
import ChatCompletionResponse from './ChatCompletionResponse.svelte'
import ChatRequest from './ChatRequest.svelte'
import { getEndpoint } from './Models.svelte'
import { getApiKey } from './Storage.svelte'
import type { ChatCompletionOpts, Request } from './Types.svelte'
export const runOpenAiCompletionRequest = async (
request: Request,
chatRequest: ChatRequest,
chatResponse: ChatCompletionResponse,
signal: AbortSignal,
opts: ChatCompletionOpts) => {
// OpenAI Request
const model = chatRequest.getModel()
const abortListener = (e:Event) => {
chatRequest.updating = false
chatRequest.updatingMessage = ''
chatResponse.updateFromError('User aborted request.')
chatRequest.removeEventListener('abort', abortListener)
}
signal.addEventListener('abort', abortListener)
const fetchOptions = {
method: 'POST',
headers: {
Authorization: `Bearer ${getApiKey()}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(request),
signal
}
if (opts.streaming) {
/**
* Streaming request/response
* We'll get the response a token at a time, as soon as they are ready
*/
chatResponse.onFinish(() => {
chatRequest.updating = false
chatRequest.updatingMessage = ''
})
fetchEventSource(getEndpoint(model), {
...fetchOptions,
openWhenHidden: true,
onmessage (ev) {
// Remove updating indicator
chatRequest.updating = 1 // hide indicator, but still signal we're updating
chatRequest.updatingMessage = ''
// console.log('ev.data', ev.data)
if (!chatResponse.hasFinished()) {
if (ev.data === '[DONE]') {
// ?? anything to do when "[DONE]"?
} else {
const data = JSON.parse(ev.data)
// console.log('data', data)
window.setTimeout(() => { chatResponse.updateFromAsyncResponse(data) }, 1)
}
}
},
onclose () {
chatRequest.updating = false
chatRequest.updatingMessage = ''
chatResponse.updateFromClose()
},
onerror (err) {
console.error(err)
throw err
},
async onopen (response) {
if (response.ok && response.headers.get('content-type') === EventStreamContentType) {
// everything's good
} else {
// client-side errors are usually non-retriable:
await chatRequest.handleError(response)
}
}
}).catch(err => {
chatRequest.updating = false
chatRequest.updatingMessage = ''
chatResponse.updateFromError(err.message)
})
} else {
/**
* Non-streaming request/response
* We'll get the response all at once, after a long delay
*/
const response = await fetch(getEndpoint(model), fetchOptions)
if (!response.ok) {
await chatRequest.handleError(response)
} else {
const json = await response.json()
// Remove updating indicator
chatRequest.updating = false
chatRequest.updatingMessage = ''
chatResponse.updateFromSyncResponse(json)
}
}
}
</script>

View File

@ -0,0 +1,139 @@
<script context="module" lang="ts">
import ChatCompletionResponse from './ChatCompletionResponse.svelte'
import ChatRequest from './ChatRequest.svelte'
import { getEndpoint, getModelDetail, getRoleTag, getStopSequence } from './Models.svelte'
import type { ChatCompletionOpts, Message, Request } from './Types.svelte'
import { getModelMaxTokens } from './Stats.svelte'
import { updateMessages } from './Storage.svelte'
export const runPetalsCompletionRequest = async (
request: Request,
chatRequest: ChatRequest,
chatResponse: ChatCompletionResponse,
signal: AbortSignal,
opts: ChatCompletionOpts) => {
// Petals
const chat = chatRequest.getChat()
const model = chatRequest.getModel()
const modelDetail = getModelDetail(model)
const ws = new WebSocket(getEndpoint(model))
const abortListener = (e:Event) => {
chatRequest.updating = false
chatRequest.updatingMessage = ''
chatResponse.updateFromError('User aborted request.')
signal.removeEventListener('abort', abortListener)
ws.close()
}
signal.addEventListener('abort', abortListener)
const stopSequences = (modelDetail.stop || ['###', '</s>']).slice()
const stopSequence = getStopSequence(chat)
let stopSequenceC = stopSequence
if (stopSequence !== '###') {
stopSequences.push(stopSequence)
stopSequenceC = '</s>'
}
const stopSequencesC = stopSequences.filter((ss) => {
return ss !== '###' && ss !== stopSequenceC
})
const maxTokens = getModelMaxTokens(model)
let maxLen = Math.min(opts.maxTokens || chatRequest.chat.max_tokens || maxTokens, maxTokens)
const promptTokenCount = chatResponse.getPromptTokenCount()
if (promptTokenCount > maxLen) {
maxLen = Math.min(maxLen + promptTokenCount, maxTokens)
}
chatResponse.onFinish(() => {
const message = chatResponse.getMessages()[0]
if (message) {
for (let i = 0, l = stopSequences.length; i < l; i++) {
const ss = stopSequences[i].trim()
if (message.content.trim().endsWith(ss)) {
message.content = message.content.trim().slice(0, message.content.trim().length - ss.length)
updateMessages(chat.id)
}
}
}
chatRequest.updating = false
chatRequest.updatingMessage = ''
})
ws.onopen = () => {
ws.send(JSON.stringify({
type: 'open_inference_session',
model,
max_length: maxLen
}))
ws.onmessage = event => {
const response = JSON.parse(event.data)
if (!response.ok) {
const err = new Error('Error opening socket: ' + response.traceback)
chatResponse.updateFromError(err.message)
console.error(err)
throw err
}
const rMessages = request.messages || [] as Message[]
// make sure top_p and temperature are set the way we need
let temperature = request.temperature
if (temperature === undefined || isNaN(temperature as any)) temperature = 1
if (!temperature || temperature <= 0) temperature = 0.01
let topP = request.top_p
if (topP === undefined || isNaN(topP as any)) topP = 1
if (!topP || topP <= 0) topP = 0.01
// build the message array
const inputArray = (rMessages).reduce((a, m) => {
const c = getRoleTag(m.role, model, chatRequest.chat) + m.content
a.push(c.trim())
return a
}, [] as string[])
const lastMessage = rMessages[rMessages.length - 1]
if (lastMessage && lastMessage.role !== 'assistant') {
inputArray.push(getRoleTag('assistant', model, chatRequest.chat))
}
const petalsRequest = {
type: 'generate',
inputs: inputArray.join(stopSequence),
max_new_tokens: 1, // wait for up to 1 tokens before displaying
stop_sequence: stopSequenceC,
do_sample: 1, // enable top p and the like
temperature,
top_p: topP
} as any
if (stopSequencesC.length) petalsRequest.extra_stop_sequences = stopSequencesC
ws.send(JSON.stringify(petalsRequest))
ws.onmessage = event => {
// Remove updating indicator
chatRequest.updating = 1 // hide indicator, but still signal we're updating
chatRequest.updatingMessage = ''
const response = JSON.parse(event.data)
if (!response.ok) {
const err = new Error('Error in response: ' + response.traceback)
console.error(err)
chatResponse.updateFromError(err.message)
throw err
}
window.setTimeout(() => {
chatResponse.updateFromAsyncResponse(
{
model,
choices: [{
delta: {
content: response.outputs,
role: 'assistant'
},
finish_reason: (response.stop ? 'stop' : null)
}]
} as any
)
}, 1)
}
}
ws.onclose = () => {
chatRequest.updating = false
chatRequest.updatingMessage = ''
chatResponse.updateFromClose()
}
ws.onerror = err => {
console.error(err)
throw err
}
}
}
</script>

View File

@ -3,7 +3,7 @@
// import { getProfile } from './Profiles.svelte'
import { cleanSettingValue, setChatSettingValue } from './Storage.svelte'
import type { Chat, ChatSetting, ChatSettings, ControlAction, FieldControl, SettingPrompt } from './Types.svelte'
import { autoGrowInputOnEvent, errorNotice } from './Util.svelte'
import { autoGrowInputOnEvent, errorNotice, valueOf } from './Util.svelte'
// import { replace } from 'svelte-spa-router'
import Fa from 'svelte-fa/src/fa.svelte'
import { openModal } from 'svelte-modals'
@ -23,6 +23,10 @@
const chatId = chat.id
let show = false
let header = valueOf(chatId, setting.header)
let headerClass = valueOf(chatId, setting.headerClass)
let placeholder = valueOf(chatId, setting.placeholder)
const buildFieldControls = () => {
fieldControls = (setting.fieldControls || [] as FieldControl[]).map(fc => {
return fc.getAction(chatId, setting, chatSettings[setting.key])
@ -38,6 +42,9 @@
afterUpdate(() => {
show = (typeof setting.hide !== 'function') || !setting.hide(chatId)
header = valueOf(chatId, setting.header)
headerClass = valueOf(chatId, setting.headerClass)
placeholder = valueOf(chatId, setting.placeholder)
buildFieldControls()
})
@ -146,9 +153,9 @@
</script>
{#if show}
{#if setting.header}
<p class="notification {setting.headerClass}">
{@html setting.header}
{#if header}
<p class="notification {headerClass}">
{@html header}
</p>
{/if}
<div class="field is-horizontal">
@ -171,7 +178,7 @@
<label class="label" for="settings-{setting.key}" title="{setting.title}">{setting.name}</label>
<textarea
class="input is-info is-focused chat-input auto-size"
placeholder={setting.placeholder || ''}
placeholder={placeholder || ''}
rows="1"
on:input={e => autoGrowInputOnEvent(e)}
on:change={e => { queueSettingValueChange(e, setting); autoGrowInputOnEvent(e) }}
@ -195,7 +202,7 @@
min={setting.min}
max={setting.max}
step={setting.step}
placeholder={String(setting.placeholder || chatDefaults[setting.key])}
placeholder={String(placeholder || chatDefaults[setting.key])}
on:change={e => queueSettingValueChange(e, setting)}
/>
{:else if setting.type === 'select' || setting.type === 'select-number'}
@ -204,7 +211,7 @@
{#key rkey}
<select id="settings-{setting.key}" title="{setting.title}" on:change={e => queueSettingValueChange(e, setting) } >
{#each setting.options as option}
<option class:is-default={option.value === chatDefaults[setting.key]} value={option.value} selected={option.value === chatSettings[setting.key]}>{option.text}</option>
<option class:is-default={option.value === chatDefaults[setting.key]} value={option.value} selected={option.value === chatSettings[setting.key]} disabled={option.disabled}>{option.text}</option>
{/each}
</select>
{/key}
@ -233,6 +240,7 @@
title="{setting.title}"
class="input"
value={chatSettings[setting.key]}
placeholder={String(placeholder || chatDefaults[setting.key])}
on:change={e => { queueSettingValueChange(e, setting) }}
>
</div>

View File

@ -3,7 +3,6 @@
import { getChatDefaults, getChatSettingList, getChatSettingObjectByKey, getExcludeFromProfile } from './Settings.svelte'
import {
saveChatStore,
apiKeyStorage,
chatsStorage,
globalStorage,
saveCustomProfile,
@ -13,7 +12,7 @@
checkStateChange,
addChat
} from './Storage.svelte'
import type { Chat, ChatSetting, ResponseModels, SettingSelect, SelectOption, ChatSettings } from './Types.svelte'
import type { Chat, ChatSetting, SettingSelect, ChatSettings } from './Types.svelte'
import { errorNotice, sizeTextElements } from './Util.svelte'
import Fa from 'svelte-fa/src/fa.svelte'
import {
@ -35,8 +34,7 @@
import { replace } from 'svelte-spa-router'
import { openModal } from 'svelte-modals'
import PromptConfirm from './PromptConfirm.svelte'
import { getApiBase, getEndpointModels } from './ApiUtil.svelte'
import { supportedModelKeys } from './Models.svelte'
import { getModelOptions } from './Models.svelte'
export let chatId:number
export const show = () => { showSettings() }
@ -185,30 +183,9 @@
// Refresh settings modal
showSettingsModal++
// Load available models from OpenAI
const allModels = (await (
await fetch(getApiBase() + getEndpointModels(), {
method: 'GET',
headers: {
Authorization: `Bearer ${$apiKeyStorage}`,
'Content-Type': 'application/json'
}
})
).json()) as ResponseModels
const filteredModels = supportedModelKeys.filter((model) => allModels.data.find((m) => m.id === model))
const modelOptions:SelectOption[] = filteredModels.reduce((a, m) => {
const o:SelectOption = {
value: m,
text: m
}
a.push(o)
return a
}, [] as SelectOption[])
// Update the models in the settings
if (modelSetting) {
modelSetting.options = modelOptions
modelSetting.options = await getModelOptions()
}
// Refresh settings modal
showSettingsModal++

View File

@ -11,6 +11,7 @@
import { openModal } from 'svelte-modals'
import PromptConfirm from './PromptConfirm.svelte'
import { getImage } from './ImageStore.svelte'
import { getModelDetail } from './Models.svelte'
export let message:Message
export let chatId:number
@ -245,7 +246,7 @@
<p class="is-size-7 message-note">System Prompt</p>
{:else if message.usage}
<p class="is-size-7 message-note">
<em>{message.model || defaultModel}</em> using <span class="has-text-weight-bold">{message.usage.total_tokens}</span>
<em>{getModelDetail(message.model || '').label || message.model || defaultModel}</em> using <span class="has-text-weight-bold">{message.usage.total_tokens}</span>
tokens ~= <span class="has-text-weight-bold">${getPrice(message.usage, message.model || defaultModel).toFixed(6)}</span>
</p>
{/if}

View File

@ -1,16 +1,22 @@
<script lang="ts">
import { apiKeyStorage, lastChatId, getChat, started } from './Storage.svelte'
import { apiKeyStorage, globalStorage, lastChatId, getChat, started, setGlobalSettingValueByKey, hasActiveModels, checkStateChange } from './Storage.svelte'
import Footer from './Footer.svelte'
import { replace } from 'svelte-spa-router'
import { onMount } from 'svelte'
import { afterUpdate, onMount } from 'svelte'
import { getPetals } from './ApiUtil.svelte'
import { clearModelOptionCache } from './Models.svelte'
$: apiKey = $apiKeyStorage
let showPetalsSettings = $globalStorage.enablePetals
let pedalsEndpoint = $globalStorage.pedalsEndpoint
let hasModels = hasActiveModels()
onMount(() => {
if (!$started) {
$started = true
// console.log('started', apiKey, $lastChatId, getChat($lastChatId))
if (apiKey && getChat($lastChatId)) {
if (hasActiveModels() && getChat($lastChatId)) {
const chatId = $lastChatId
$lastChatId = 0
replace(`/chat/${chatId}`)
@ -19,21 +25,39 @@ onMount(() => {
$lastChatId = 0
})
afterUpdate(() => {
clearModelOptionCache()
hasModels = hasActiveModels()
pedalsEndpoint = $globalStorage.pedalsEndpoint
$checkStateChange++
})
const setPetalsEnabled = (event: Event) => {
const el = (event.target as HTMLInputElement)
setGlobalSettingValueByKey('enablePetals', !!el.checked)
showPetalsSettings = $globalStorage.enablePetals
}
</script>
<section class="section">
<article class="message">
<div class="message-body">
<strong><a href="https://github.com/Niek/chatgpt-web">ChatGPT-web</a></strong>
<p class="mb-4">
<strong><a href="https://github.com/Niek/chatgpt-web" target="_blank">ChatGPT-web</a></strong>
is a simple one-page web interface to the OpenAI ChatGPT API. To use it, you need to register for
<a href="https://platform.openai.com/account/api-keys" target="_blank" rel="noreferrer">an OpenAI API key</a>
first. OpenAI bills per token (usage-based), which means it is a lot cheaper than
<a href="https://openai.com/blog/chatgpt-plus" target="_blank" rel="noreferrer">ChatGPT Plus</a>, unless you use
more than 10 million tokens per month. All messages are stored in your browser's local storage, so everything is
<strong>private</strong>. You can also close the browser tab and come back later to continue the conversation.
</p>
<p>
As an alternative to OpenAI, you can also use Petals swarm as a free API option for open chat models like Llama 2.
</p>
</div>
</article>
<article class="message" class:is-danger={!apiKey} class:is-warning={apiKey}>
<article class="message" class:is-danger={!hasModels} class:is-warning={!apiKey} class:is-info={apiKey}>
<div class="message-body">
Set your OpenAI API key below:
@ -53,19 +77,81 @@ onMount(() => {
type="password"
autocomplete="off"
class="input"
class:is-danger={!apiKey}
class:is-danger={!hasModels}
class:is-warning={!apiKey} class:is-info={apiKey}
value={apiKey}
/>
</p>
<p class="control">
<button class="button is-info" type="submit">Save</button>
</p>
</form>
{#if !apiKey}
<p class="help is-danger">
Please enter your <a href="https://platform.openai.com/account/api-keys">OpenAI API key</a> above to use ChatGPT-web.
It is required to use ChatGPT-web.
<p class:is-danger={!hasModels} class:is-warning={!apiKey}>
Please enter your <a target="_blank" href="https://platform.openai.com/account/api-keys">OpenAI API key</a> above to use Open AI's ChatGPT API.
At least one API must be enabled to use ChatGPT-web.
</p>
{/if}
</div>
</article>
<article class="message" class:is-danger={!hasModels} class:is-warning={!showPetalsSettings} class:is-info={showPetalsSettings}>
<div class="message-body">
<label class="label" for="enablePetals">
<input
type="checkbox"
class="checkbox"
id="enablePetals"
checked={!!$globalStorage.enablePetals}
on:click={setPetalsEnabled}
>
Use Petals API and Models (Llama 2)
</label>
{#if showPetalsSettings}
<p>Set Petals API Endpoint:</p>
<form
class="field has-addons has-addons-right"
on:submit|preventDefault={(event) => {
if (event.target && event.target[0].value) {
setGlobalSettingValueByKey('pedalsEndpoint', (event.target[0].value).trim())
} else {
setGlobalSettingValueByKey('pedalsEndpoint', '')
}
}}
>
<p class="control is-expanded">
<input
aria-label="PetalsAPI Endpoint"
type="text"
class="input"
placeholder={getPetals()}
value={$globalStorage.pedalsEndpoint || ''}
/>
</p>
<p class="control">
<button class="button is-info" type="submit">Save</button>
</p>
</form>
{#if !pedalsEndpoint}
<p class="help is-warning">
Please only use the default public API for testing. It's best to <a target="_blank" href="https://github.com/petals-infra/chat.petals.dev">configure a private endpoint</a> and enter it above for connection to the Petals swarm.
</p>
{/if}
<p class="my-4">
<a target="_blank" href="https://petals.dev/">Petals</a> lets you run large language models at home by connecting to a public swarm, BitTorrent-style, without hefty GPU requirements.
</p>
<p class="mb-4">
You are encouraged to <a target="_blank" href="https://github.com/bigscience-workshop/petals/wiki/FAQ:-Frequently-asked-questions#running-a-server">set up a Petals server to share your GPU resources</a> with the public swarm. Minimum requirements to contribute Llama 2 completions are a GTX&nbsp;1080&nbsp;8GB, but the larger/faster the better.
</p>
<p class="help is-warning">
Because Petals uses a public swarm, <b>do not send sensitive information</b> when using Petals.
</p>
{/if}
</div>

View File

@ -1,43 +1,108 @@
<script context="module" lang="ts">
import type { ModelDetail, Model } from './Types.svelte'
import { getApiBase, getEndpointCompletions, getEndpointGenerations, getEndpointModels, getPetals } from './ApiUtil.svelte'
import { apiKeyStorage, globalStorage } from './Storage.svelte'
import { get, writable } from 'svelte/store'
import type { ModelDetail, Model, ResponseModels, SelectOption, Chat } from './Types.svelte'
import { encode } from 'gpt-tokenizer'
import llamaTokenizer from 'llama-tokenizer-js'
import { mergeProfileFields } from './Profiles.svelte'
import { getChatSettingObjectByKey } from './Settings.svelte'
import { valueOf } from './Util.svelte'
/**
* TODO: All of this + what's scattered about need to be refactored to interfaces and classes
* to make it all more modular
*/
const modelOptionCache = writable([] as SelectOption[])
// Reference: https://openai.com/pricing#language-models
// Eventually we'll add API hosts and endpoints to this
const modelDetails : Record<string, ModelDetail> = {
'gpt-4-32k': {
type: 'OpenAIChat',
prompt: 0.00006, // $0.06 per 1000 tokens prompt
completion: 0.00012, // $0.12 per 1000 tokens completion
max: 32768 // 32k max token buffer
},
'gpt-4': {
type: 'OpenAIChat',
prompt: 0.00003, // $0.03 per 1000 tokens prompt
completion: 0.00006, // $0.06 per 1000 tokens completion
max: 8192 // 8k max token buffer
},
'gpt-3.5': {
type: 'OpenAIChat',
prompt: 0.0000015, // $0.0015 per 1000 tokens prompt
completion: 0.000002, // $0.002 per 1000 tokens completion
max: 4096 // 4k max token buffer
},
'gpt-3.5-turbo-16k': {
type: 'OpenAIChat',
prompt: 0.000003, // $0.003 per 1000 tokens prompt
completion: 0.000004, // $0.004 per 1000 tokens completion
max: 16384 // 16k max token buffer
},
'enoch/llama-65b-hf': {
type: 'Petals',
label: 'Petals - Llama-65b',
stop: ['###', '</s>'],
userStart: '<|user|>',
assistantStart: '<|[[CHARACTER_NAME]]|>',
systemStart: '',
prompt: 0.000000, // $0.000 per 1000 tokens prompt
completion: 0.000000, // $0.000 per 1000 tokens completion
max: 2048 // 2k max token buffer
},
'timdettmers/guanaco-65b': {
type: 'Petals',
label: 'Petals - Guanaco-65b',
stop: ['###', '</s>'],
userStart: '<|user|>',
assistantStart: '<|[[CHARACTER_NAME]]|>',
systemStart: '',
prompt: 0.000000, // $0.000 per 1000 tokens prompt
completion: 0.000000, // $0.000 per 1000 tokens completion
max: 2048 // 2k max token buffer
},
'meta-llama/Llama-2-70b-chat-hf': {
type: 'Petals',
label: 'Petals - Llama-2-70b-chat',
stop: ['###', '</s>'],
userStart: '<|user|>',
assistantStart: '<|[[CHARACTER_NAME]]|>',
systemStart: '',
prompt: 0.000000, // $0.000 per 1000 tokens prompt
completion: 0.000000, // $0.000 per 1000 tokens completion
max: 4096 // 4k max token buffer
},
'meta-llama/Llama-2-70b-hf': {
type: 'Petals',
label: 'Petals - Llama-2-70b',
stop: ['###', '</s>'],
userStart: '<|user|>',
assistantStart: '<|[[CHARACTER_NAME]]|>',
systemStart: '',
prompt: 0.000000, // $0.000 per 1000 tokens prompt
completion: 0.000000, // $0.000 per 1000 tokens completion
max: 4096 // 4k max token buffer
}
}
const imageModels : Record<string, ModelDetail> = {
export const imageModels : Record<string, ModelDetail> = {
'dall-e-1024x1024': {
type: 'OpenAIDall-e',
prompt: 0.00,
completion: 0.020, // $0.020 per image
max: 1000 // 1000 char prompt, max
},
'dall-e-512x512': {
type: 'OpenAIDall-e',
prompt: 0.00,
completion: 0.018, // $0.018 per image
max: 1000 // 1000 char prompt, max
},
'dall-e-256x256': {
type: 'OpenAIDall-e',
prompt: 0.00,
completion: 0.016, // $0.016 per image
max: 1000 // 1000 char prompt, max
@ -47,22 +112,27 @@ const imageModels : Record<string, ModelDetail> = {
const unknownDetail = {
prompt: 0,
completion: 0,
max: 4096
}
max: 4096,
type: 'OpenAIChat'
} as ModelDetail
// See: https://platform.openai.com/docs/models/model-endpoint-compatibility
// Eventually we'll add UI for managing this
export const supportedModels : Record<string, ModelDetail> = {
'gpt-3.5-turbo': modelDetails['gpt-3.5'],
'gpt-3.5-turbo-0301': modelDetails['gpt-3.5'],
'gpt-3.5-turbo-0613': modelDetails['gpt-3.5'],
'gpt-3.5-turbo-16k': modelDetails['gpt-3.5-turbo-16k'],
'gpt-4': modelDetails['gpt-4'],
'gpt-4-0314': modelDetails['gpt-4'],
'gpt-4-0613': modelDetails['gpt-4'],
'gpt-4-32k': modelDetails['gpt-4-32k'],
'gpt-4-32k-0314': modelDetails['gpt-4-32k'],
'gpt-4-32k-0613': modelDetails['gpt-4-32k'],
'gpt-3.5-turbo': modelDetails['gpt-3.5'],
'gpt-3.5-turbo-16k': modelDetails['gpt-3.5-turbo-16k'],
'gpt-3.5-turbo-0301': modelDetails['gpt-3.5'],
'gpt-3.5-turbo-0613': modelDetails['gpt-3.5']
// 'enoch/llama-65b-hf': modelDetails['enoch/llama-65b-hf'],
// 'timdettmers/guanaco-65b': modelDetails['timdettmers/guanaco-65b'],
'meta-llama/Llama-2-70b-hf': modelDetails['meta-llama/Llama-2-70b-hf'],
'meta-llama/Llama-2-70b-chat-hf': modelDetails['meta-llama/Llama-2-70b-chat-hf']
}
const lookupList = {
@ -75,7 +145,7 @@ export const supportedModelKeys = Object.keys({ ...supportedModels, ...imageMode
const tpCache : Record<string, ModelDetail> = {}
export const getModelDetail = (model: Model) => {
export const getModelDetail = (model: Model): ModelDetail => {
// First try to get exact match, then from cache
let r = supportedModels[model] || tpCache[model]
if (r) return r
@ -93,4 +163,140 @@ export const getModelDetail = (model: Model) => {
return r
}
export const getEndpoint = (model: Model): string => {
const modelDetails = getModelDetail(model)
const gSettings = get(globalStorage)
switch (modelDetails.type) {
case 'Petals':
return gSettings.pedalsEndpoint || getPetals()
case 'OpenAIDall-e':
return getApiBase() + getEndpointGenerations()
case 'OpenAIChat':
default:
return gSettings.openAICompletionEndpoint || (getApiBase() + getEndpointCompletions())
}
}
export const getStopSequence = (chat: Chat): string => {
return chat.settings.stopSequence || valueOf(chat.id, getChatSettingObjectByKey('stopSequence').placeholder)
}
export const getUserStart = (chat: Chat): string => {
return mergeProfileFields(
chat.settings,
chat.settings.userMessageStart || valueOf(chat.id, getChatSettingObjectByKey('userMessageStart').placeholder)
)
}
export const getAssistantStart = (chat: Chat): string => {
return mergeProfileFields(
chat.settings,
chat.settings.assistantMessageStart || valueOf(chat.id, getChatSettingObjectByKey('assistantMessageStart').placeholder)
)
}
export const getSystemStart = (chat: Chat): string => {
return mergeProfileFields(
chat.settings,
chat.settings.systemMessageStart || valueOf(chat.id, getChatSettingObjectByKey('systemMessageStart').placeholder)
)
}
export const getRoleTag = (role: string, model: Model, chat: Chat): string => {
const modelDetails = getModelDetail(model)
switch (modelDetails.type) {
case 'Petals':
if (role === 'assistant') return getAssistantStart(chat) + ' '
if (role === 'user') return getUserStart(chat) + ' '
return getSystemStart(chat) + ' '
case 'OpenAIDall-e':
return role
case 'OpenAIChat':
default:
return role
}
}
export const getTokens = (model: Model, value: string): number[] => {
const modelDetails = getModelDetail(model)
switch (modelDetails.type) {
case 'Petals':
return llamaTokenizer.encode(value)
case 'OpenAIDall-e':
return [0]
case 'OpenAIChat':
default:
return encode(value)
}
}
export const countTokens = (model: Model, value: string): number => {
return getTokens(model, value).length
}
export const clearModelOptionCache = () => {
modelOptionCache.set([])
}
export async function getModelOptions (): Promise<SelectOption[]> {
const gSettings = get(globalStorage)
const openAiKey = get(apiKeyStorage)
const cachedOptions = get(modelOptionCache)
if (cachedOptions && cachedOptions.length) return cachedOptions
// Load available models from OpenAI
let openAiModels
let allowCache = true
if (openAiKey) {
try {
openAiModels = (await (
await fetch(getApiBase() + getEndpointModels(), {
method: 'GET',
headers: {
Authorization: `Bearer ${openAiKey}`,
'Content-Type': 'application/json'
}
})
).json()) as ResponseModels
} catch (e) {
allowCache = false
openAiModels = { data: [] }
}
} else {
openAiModels = { data: [] }
}
// const filteredModels = Object.keys(supportedModels).filter((model) => {
// switch (getModelDetail(model).type) {
// case 'Petals':
// return gSettings.enablePetals
// case 'OpenAIChat':
// default:
// return openAiModels.data && openAiModels.data.find((m) => m.id === model)
// }
// })
const modelOptions:SelectOption[] = Object.keys(supportedModels).reduce((a, m) => {
let disabled
const modelDetail = getModelDetail(m)
switch (modelDetail.type) {
case 'Petals':
disabled = !gSettings.enablePetals
break
case 'OpenAIChat':
default:
disabled = !(openAiModels.data && openAiModels.data.find((m) => m.id === m))
}
const o:SelectOption = {
value: m,
text: modelDetail.label || m,
disabled
}
a.push(o)
return a
}, [] as SelectOption[])
if (allowCache) modelOptionCache.set(modelOptions)
return modelOptions
}
</script>

View File

@ -1,5 +1,5 @@
<script context="module" lang="ts">
import { getChatDefaults, getExcludeFromProfile } from './Settings.svelte'
import { getChatDefaults, getDefaultModel, getExcludeFromProfile } from './Settings.svelte'
import { get, writable } from 'svelte/store'
// Profile definitions
import { addMessage, clearMessages, deleteMessage, getChat, getChatSettings, getCustomProfiles, getGlobalSettings, getMessages, newName, resetChatSettings, saveChatStore, setGlobalSettingValueByKey, setMessages, updateProfile } from './Storage.svelte'
@ -22,7 +22,9 @@ export const getProfiles = (forceUpdate:boolean = false):Record<string, ChatSett
}
const result = Object.entries(profiles
).reduce((a, [k, v]) => {
v = JSON.parse(JSON.stringify(v))
a[k] = v
v.model = v.model || getDefaultModel()
return a
}, {} as Record<string, ChatSettings>)
Object.entries(getCustomProfiles()).forEach(([k, v]) => {
@ -72,7 +74,7 @@ export const getProfile = (key:string, forReset:boolean = false):ChatSettings =>
export const mergeProfileFields = (settings: ChatSettings, content: string|undefined, maxWords: number|undefined = undefined): string => {
if (!content?.toString) return ''
content = (content + '').replaceAll('[[CHARACTER_NAME]]', settings.characterName || 'ChatGPT')
content = (content + '').replaceAll('[[CHARACTER_NAME]]', settings.characterName || 'Assistant')
if (maxWords) content = (content + '').replaceAll('[[MAX_WORDS]]', maxWords.toString())
return content
}

View File

@ -1,7 +1,7 @@
<script context="module" lang="ts">
import { applyProfile } from './Profiles.svelte'
import { getChatSettings, getGlobalSettings, setGlobalSettingValueByKey } from './Storage.svelte'
import { encode } from 'gpt-tokenizer'
import { get } from 'svelte/store'
import { apiKeyStorage, getChatSettings, getGlobalSettings, setGlobalSettingValueByKey } from './Storage.svelte'
import { faArrowDown91, faArrowDownAZ, faCheck, faThumbTack } from '@fortawesome/free-solid-svg-icons/index'
// Setting definitions
@ -18,8 +18,15 @@ import {
type ChatSortOption
} from './Types.svelte'
import { getModelDetail, getTokens } from './Models.svelte'
export const defaultModel:Model = 'gpt-3.5-turbo'
const defaultModel:Model = 'gpt-3.5-turbo'
const defaultModelPetals:Model = 'meta-llama/Llama-2-70b-chat-hf'
export const getDefaultModel = (): Model => {
if (!get(apiKeyStorage)) return defaultModelPetals
return defaultModel
}
export const getChatSettingList = (): ChatSetting[] => {
return chatSettingsList
@ -55,8 +62,16 @@ export const getExcludeFromProfile = () => {
return excludeFromProfile
}
const isNotOpenAI = (chatId) => {
return getModelDetail(getChatSettings(chatId).model).type !== 'OpenAIChat'
}
const isNotPetals = (chatId) => {
return getModelDetail(getChatSettings(chatId).model).type !== 'Petals'
}
const gptDefaults = {
model: defaultModel,
model: '',
messages: [],
temperature: 1,
top_p: 1,
@ -94,6 +109,10 @@ const defaults:ChatSettings = {
hppContinuePrompt: '',
hppWithSummaryPrompt: false,
imageGenerationSize: '',
stopSequence: '',
userMessageStart: '',
assistantMessageStart: '',
systemMessageStart: '',
// useResponseAlteration: false,
// responseAlterations: [],
isDirty: false
@ -104,7 +123,10 @@ export const globalDefaults: GlobalSettings = {
lastProfile: 'default',
defaultProfile: 'default',
hideSummarized: false,
chatSort: 'created'
chatSort: 'created',
openAICompletionEndpoint: '',
enablePetals: false,
pedalsEndpoint: ''
}
const excludeFromProfile = {
@ -399,7 +421,13 @@ const modelSetting: ChatSetting & SettingSelect = {
key: 'model',
name: 'Model',
title: 'The model to use - GPT-3.5 is cheaper, but GPT-4 is more powerful.',
header: 'Below are the settings that OpenAI allows to be changed for the API calls. See the <a target="_blank" href="https://platform.openai.com/docs/api-reference/chat/create">OpenAI API docs</a> for more details.',
header: (chatId) => {
if (isNotOpenAI(chatId)) {
return 'Below are the settings that can be changed for the API calls. See <a target="_blank" href="https://platform.openai.com/docs/api-reference/chat/create">this overview</a> to start, though not all settings translate to Petals.'
} else {
return 'Below are the settings that OpenAI allows to be changed for the API calls. See the <a target="_blank" href="https://platform.openai.com/docs/api-reference/chat/create">OpenAI API docs</a> for more details.'
}
},
headerClass: 'is-warning',
options: [],
type: 'select',
@ -417,7 +445,8 @@ const chatSettingsList: ChatSetting[] = [
key: 'stream',
name: 'Stream Response',
title: 'Stream responses as they are generated.',
type: 'boolean'
type: 'boolean',
hide: isNotOpenAI
},
{
key: 'temperature',
@ -432,7 +461,7 @@ const chatSettingsList: ChatSetting[] = [
},
{
key: 'top_p',
name: 'Nucleus Sampling',
name: 'Nucleus Sampling (Top-p)',
title: 'An alternative to sampling with temperature, called nucleus sampling, where the model considers the results of the tokens with top_p probability mass. So 0.1 means only the tokens comprising the top 10% probability mass are considered.\n' +
'\n' +
'We generally recommend altering this or temperature but not both',
@ -448,7 +477,8 @@ const chatSettingsList: ChatSetting[] = [
min: 1,
max: 10,
step: 1,
type: 'number'
type: 'number',
hide: isNotOpenAI
},
{
key: 'max_tokens',
@ -460,6 +490,7 @@ const chatSettingsList: ChatSetting[] = [
max: 32768,
step: 1,
type: 'number',
hide: isNotOpenAI,
forceApi: true // Since default here is different than gpt default, will make sure we always send it
},
{
@ -469,7 +500,8 @@ const chatSettingsList: ChatSetting[] = [
min: -2,
max: 2,
step: 0.2,
type: 'number'
type: 'number',
hide: isNotOpenAI
},
{
key: 'frequency_penalty',
@ -478,7 +510,52 @@ const chatSettingsList: ChatSetting[] = [
min: -2,
max: 2,
step: 0.2,
type: 'number'
type: 'number',
hide: isNotOpenAI
},
{
key: 'stopSequence',
name: 'Stop Sequence',
title: 'Characters used to separate messages in the message chain.',
type: 'text',
placeholder: (chatId) => {
const val = getModelDetail(getChatSettings(chatId).model).stop
return (val && val[0]) || ''
},
hide: isNotPetals
},
{
key: 'userMessageStart',
name: 'User Message Start Sequence',
title: 'Sequence to denote user messages in the message chain.',
type: 'text',
placeholder: (chatId) => {
const val = getModelDetail(getChatSettings(chatId).model).userStart
return val || ''
},
hide: isNotPetals
},
{
key: 'assistantMessageStart',
name: 'Assistant Message Start Sequence',
title: 'Sequence to denote assistant messages in the message chain.',
type: 'text',
placeholder: (chatId) => {
const val = getModelDetail(getChatSettings(chatId).model).assistantStart
return val || ''
},
hide: isNotPetals
},
{
key: 'systemMessageStart',
name: 'System Message Start Sequence',
title: 'Sequence to denote system messages in the message chain.',
type: 'text',
placeholder: (chatId) => {
const val = getModelDetail(getChatSettings(chatId).model).systemStart
return val || ''
},
hide: isNotPetals
},
{
// logit bias editor not implemented yet
@ -497,7 +574,7 @@ const chatSettingsList: ChatSetting[] = [
// console.log('logit_bias', val, getChatSettings(chatId).logit_bias)
if (!val) return null
const tokenized:Record<number, number> = Object.entries(val).reduce((a, [k, v]) => {
const tokens:number[] = encode(k)
const tokens:number[] = getTokens(getChatSettings(chatId).model, k)
tokens.forEach(t => { a[t] = v })
return a
}, {} as Record<number, number>)
@ -536,6 +613,21 @@ const globalSettingsList:GlobalSetting[] = [
key: 'hideSummarized',
name: 'Hide Summarized Messages',
type: 'boolean'
},
{
key: 'openAICompletionEndpoint',
name: 'OpenAI Completions Endpoint',
type: 'text'
},
{
key: 'enablePetals',
name: 'Enable Petals APIs',
type: 'boolean'
},
{
key: 'pedalsEndpoint',
name: 'Petals API Endpoint',
type: 'text'
}
]

View File

@ -1,7 +1,7 @@
<script lang="ts">
import { params } from 'svelte-spa-router'
import ChatMenuItem from './ChatMenuItem.svelte'
import { apiKeyStorage, chatsStorage, pinMainMenu, checkStateChange, getChatSortOption, setChatSortOption } from './Storage.svelte'
import { apiKeyStorage, chatsStorage, pinMainMenu, checkStateChange, getChatSortOption, setChatSortOption, hasActiveModels } from './Storage.svelte'
import Fa from 'svelte-fa/src/fa.svelte'
import { faSquarePlus, faKey } from '@fortawesome/free-solid-svg-icons/index'
import ChatOptionMenu from './ChatOptionMenu.svelte'
@ -14,10 +14,12 @@
$: activeChatId = $params && $params.chatId ? parseInt($params.chatId) : undefined
let sortOption = getChatSortOption()
let hasModels = hasActiveModels()
const onStateChange = (...args:any) => {
sortOption = getChatSortOption()
sortedChats = $chatsStorage.sort(sortOption.sortFn)
hasModels = hasActiveModels()
}
$: onStateChange($checkStateChange)
@ -72,14 +74,14 @@
</div>
</div>
<div class="level-right">
{#if !$apiKeyStorage}
{#if !hasModels}
<div class="level-item">
<a href={'#/'} class="panel-block" class:is-disabled={!$apiKeyStorage}
><span class="greyscale mr-1"><Fa icon={faKey} /></span> API key</a
></div>
{:else}
<div class="level-item">
<button on:click={() => { $pinMainMenu = false; startNewChatWithWarning(activeChatId) }} class="panel-block button" title="Start new chat with default profile" class:is-disabled={!$apiKeyStorage}
<button on:click={() => { $pinMainMenu = false; startNewChatWithWarning(activeChatId) }} class="panel-block button" title="Start new chat with default profile" class:is-disabled={!hasModels}
><span class="greyscale mr-1"><Fa icon={faSquarePlus} /></span> New chat</button>
</div>
{/if}

View File

@ -1,25 +1,43 @@
<script context="module" lang="ts">
import { getModelDetail } from './Models.svelte'
import type { Message, Model, Usage } from './Types.svelte'
import { encode } from 'gpt-tokenizer'
import { countTokens, getModelDetail, getRoleTag, getStopSequence } from './Models.svelte'
import type { Chat, Message, Model, Usage } from './Types.svelte'
export const getPrice = (tokens: Usage, model: Model): number => {
const t = getModelDetail(model)
return ((tokens.prompt_tokens * t.prompt) + (tokens.completion_tokens * t.completion))
}
export const countPromptTokens = (prompts:Message[], model:Model):number => {
return prompts.reduce((a, m) => {
a += countMessageTokens(m, model)
export const countPromptTokens = (prompts:Message[], model:Model, chat: Chat):number => {
const detail = getModelDetail(model)
const count = prompts.reduce((a, m) => {
a += countMessageTokens(m, model, chat)
return a
}, 0) + 3 // Always seems to be message counts + 3
}, 0)
switch (detail.type) {
case 'Petals':
return count
case 'OpenAIChat':
default:
// Not sure how OpenAI formats it, but this seems to get close to the right counts.
// Would be nice to know. This works for gpt-3.5. gpt-4 could be different.
// Complete stab in the dark here -- update if you know where all the extra tokens really come from.
return count + 3 // Always seems to be message counts + 3
}
}
export const countMessageTokens = (message:Message, model:Model):number => {
// Not sure how OpenAI formats it, but this seems to get close to the right counts.
// Would be nice to know. This works for gpt-3.5. gpt-4 could be different.
// Complete stab in the dark here -- update if you know where all the extra tokens really come from.
return encode('## ' + message.role + ' ##:\r\n\r\n' + message.content + '\r\n\r\n\r\n').length
export const countMessageTokens = (message:Message, model:Model, chat: Chat):number => {
const detail = getModelDetail(model)
const stop = getStopSequence(chat)
switch (detail.type) {
case 'Petals':
return countTokens(model, getRoleTag(message.role, model, chat) + ': ' + message.content + (stop || '###'))
case 'OpenAIChat':
default:
// Not sure how OpenAI formats it, but this seems to get close to the right counts.
// Would be nice to know. This works for gpt-3.5. gpt-4 could be different.
// Complete stab in the dark here -- update if you know where all the extra tokens really come from.
return countTokens(model, '## ' + message.role + ' ##:\r\n\r\n' + message.content + '\r\n\r\n\r\n')
}
}
export const getModelMaxTokens = (model:Model):number => {

View File

@ -30,6 +30,11 @@
return get(apiKeyStorage)
}
export const hasActiveModels = (): boolean => {
const globalSettings = get(globalStorage) || {}
return !!get(apiKeyStorage) || !!globalSettings.enablePetals
}
export const newChatID = (): number => {
const chats = get(chatsStorage)
const chatId = chats.reduce((maxId, chat) => Math.max(maxId, chat.id), 0) + 1

View File

@ -7,7 +7,15 @@ export type Model = typeof supportedModelKeys[number];
export type ImageGenerationSizes = typeof imageGenerationSizeTypes[number];
export type RequestType = 'OpenAIChat' | 'OpenAIDall-e' | 'Petals'
export type ModelDetail = {
type: RequestType;
label?: string;
stop?: string[];
userStart?: string,
assistantStart?: string,
systemStart?: string,
prompt: number;
completion: number;
max: number;
@ -105,6 +113,10 @@ export type ChatSettings = {
trainingPrompts?: Message[];
useResponseAlteration?: boolean;
responseAlterations?: ResponseAlteration[];
stopSequence: string;
userMessageStart: string;
assistantMessageStart: string;
systemMessageStart: string;
isDirty?: boolean;
} & Request;
@ -122,16 +134,16 @@ export type Chat = {
};
type ResponseOK = {
id: string;
object: string;
created: number;
choices: {
index: number;
id?: string;
object?: string;
created?: number;
choices?: {
index?: number;
message: Message;
finish_reason: string;
finish_reason?: string;
delta: Message;
}[];
usage: Usage;
usage?: Usage;
model: Model;
};
@ -172,6 +184,9 @@ export type GlobalSettings = {
defaultProfile: string;
hideSummarized: boolean;
chatSort: ChatSortOptions;
openAICompletionEndpoint: string;
enablePetals: boolean;
pedalsEndpoint: string;
};
type SettingNumber = {
@ -184,6 +199,7 @@ export type GlobalSettings = {
export type SelectOption = {
value: string|number;
text: string;
disabled?: boolean;
};
export type ChatSortOption = SelectOption & {
@ -236,15 +252,17 @@ export type SubSetting = {
settings: any[];
};
export type ValueFn = (chatId:number) => string
export type ChatSetting = {
key: keyof ChatSettings;
name: string;
title: string;
forceApi?: boolean; // force in api requests, even if set to default
hidden?: boolean; // Hide from setting menus
header?: string;
headerClass?: string;
placeholder?: string;
header?: string | ValueFn;
headerClass?: string | ValueFn;
placeholder?: string | ValueFn;
hide?: (chatId:number) => boolean;
apiTransform?: (chatId:number, setting:ChatSetting, value:any) => any;
fieldControls?: FieldControl[];

View File

@ -147,4 +147,9 @@
newChat()
}
export const valueOf = (chatId: number, value: any) => {
if (typeof value === 'function') return value(chatId)
return value
}
</script>