WIP, custom API Modes

This commit is contained in:
josc146
2024-08-06 19:24:21 +08:00
parent 34cfb5c9ba
commit e4cd8381e3
16 changed files with 368 additions and 59 deletions
+9 -10
View File
@@ -3,7 +3,13 @@ import PropTypes from 'prop-types'
import Browser from 'webextension-polyfill'
import InputBox from '../InputBox'
import ConversationItem from '../ConversationItem'
import { createElementAtPosition, isFirefox, isMobile, isSafari } from '../../utils'
import {
createElementAtPosition,
isFirefox,
isMobile,
isSafari,
modelNameToDesc,
} from '../../utils'
import {
ArchiveIcon,
DesktopDownloadIcon,
@@ -16,7 +22,7 @@ import FileSaver from 'file-saver'
import { render } from 'preact'
import FloatingToolbar from '../FloatingToolbar'
import { useClampWindowSize } from '../../hooks/use-clamp-window-size'
import { bingWebModelKeys, getUserConfig, ModelMode, Models } from '../../config/index.mjs'
import { bingWebModelKeys, getUserConfig, Models } from '../../config/index.mjs'
import { useTranslation } from 'react-i18next'
import DeleteButton from '../DeleteButton'
import { useConfig } from '../../hooks/use-config.mjs'
@@ -370,14 +376,7 @@ function ConversationCard(props) {
}}
>
{config.activeApiModes.map((modelName) => {
let desc
if (modelName.includes('-')) {
const splits = modelName.split('-')
if (splits[0] in Models)
desc = `${t(Models[splits[0]].desc)} (${t(ModelMode[splits[1]])})`
} else {
if (modelName in Models) desc = t(Models[modelName].desc)
}
const desc = modelNameToDesc(modelName, t)
if (desc)
return (
<option
+78
View File
@@ -82,6 +82,73 @@ export const poeWebModelKeys = [
]
export const moonshotApiModelKeys = ['moonshot_v1_8k', 'moonshot_v1_32k', 'moonshot_v1_128k']
export const AlwaysCustomGroups = [
'ollamaApiModelKeys',
'customApiModelKeys',
'azureOpenAiApiModelKeys',
]
export const CustomUrlGroups = ['customApiModelKeys']
export const CustomApiKeyGroups = ['customApiModelKeys']
export const ModelGroups = {
chatgptWebModelKeys: {
value: chatgptWebModelKeys,
desc: 'ChatGPT (Web)',
},
claudeWebModelKeys: {
value: claudeWebModelKeys,
desc: 'Claude.ai (Web)',
},
moonshotWebModelKeys: {
value: moonshotWebModelKeys,
desc: 'Kimi.Moonshot (Web)',
},
bingWebModelKeys: {
value: bingWebModelKeys,
desc: 'Bing (Web)',
},
bardWebModelKeys: {
value: bardWebModelKeys,
desc: 'Gemini (Web)',
},
chatgptApiModelKeys: {
value: chatgptApiModelKeys,
desc: 'ChatGPT (API)',
},
claudeApiModelKeys: {
value: claudeApiModelKeys,
desc: 'Claude.ai (API)',
},
moonshotApiModelKeys: {
value: moonshotApiModelKeys,
desc: 'Kimi.Moonshot (API)',
},
chatglmApiModelKeys: {
value: chatglmApiModelKeys,
desc: 'ChatGLM (API)',
},
ollamaApiModelKeys: {
value: ollamaApiModelKeys,
desc: 'Ollama (API)',
},
azureOpenAiApiModelKeys: {
value: azureOpenAiApiModelKeys,
desc: 'ChatGPT (Azure API)',
},
gptApiModelKeys: {
value: gptApiModelKeys,
desc: 'GPT Completion (API)',
},
githubThirdPartyApiModelKeys: {
value: githubThirdPartyApiModelKeys,
desc: 'Github Third Party Waylaidwanderer (API)',
},
customApiModelKeys: {
value: customApiModelKeys,
desc: 'Custom Model',
},
}
/**
* @typedef {object} Model
* @property {string} value
@@ -290,6 +357,17 @@ export const defaultConfig = {
'ollamaModel',
'azureOpenAi',
],
customApiModes: [
{
groupName: '',
itemName: '',
isCustom: false,
customName: '',
customUrl: '',
apiKey: '',
active: false,
},
],
activeSelectionTools: ['translate', 'summary', 'polish', 'code', 'ask'],
customSelectionTools: [
{
+174 -21
View File
@@ -1,43 +1,196 @@
import { useTranslation } from 'react-i18next'
import PropTypes from 'prop-types'
import { ModelMode, Models } from '../../config/index.mjs'
import { apiModeToModelName, modelNameToDesc } from '../../utils/index.mjs'
import { PencilIcon, TrashIcon } from '@primer/octicons-react'
import { useState } from 'react'
import {
AlwaysCustomGroups,
CustomApiKeyGroups,
CustomUrlGroups,
ModelGroups,
} from '../../config/index.mjs'
ApiModes.propTypes = {
config: PropTypes.object.isRequired,
updateConfig: PropTypes.func.isRequired,
}
const defaultApiMode = {
groupName: 'chatgptWebModelKeys',
itemName: 'chatgptFree35',
isCustom: false,
customName: '',
customUrl: '',
apiKey: '',
active: true,
}
export function ApiModes({ config, updateConfig }) {
const { t } = useTranslation()
const [editing, setEditing] = useState(false)
const [editingApiMode, setEditingApiMode] = useState(defaultApiMode)
const [editingIndex, setEditingIndex] = useState(-1)
const editingComponent = (
<div style={{ display: 'flex', flexDirection: 'column', '--spacing': '4px' }}>
<div style={{ display: 'flex', gap: '12px' }}>
<button
onClick={(e) => {
e.preventDefault()
setEditing(false)
}}
>
{t('Cancel')}
</button>
<button
onClick={(e) => {
e.preventDefault()
if (editingIndex === -1) {
updateConfig({
customApiModes: [...config.customApiModes, editingApiMode],
})
} else {
const customApiModes = [...config.customApiModes]
customApiModes[editingIndex] = editingApiMode
updateConfig({ customApiModes })
}
setEditing(false)
}}
>
{t('Save')}
</button>
</div>
<div style={{ display: 'flex', gap: '4px', alignItems: 'center', whiteSpace: 'noWrap' }}>
{t('Type')}
<select
value={editingApiMode.groupName}
onChange={(e) => {
const groupName = e.target.value
const itemName = ModelGroups[groupName].value[0]
setEditingApiMode({ ...editingApiMode, groupName, itemName })
}}
>
{Object.entries(ModelGroups).map(([groupName, { desc }]) => (
<option key={groupName} value={groupName}>
{t(desc)}
</option>
))}
</select>
</div>
<div style={{ display: 'flex', gap: '4px', alignItems: 'center', whiteSpace: 'noWrap' }}>
{t('Mode')}
<select
value={editingApiMode.itemName}
onChange={(e) => {
const itemName = e.target.value
const isCustom = itemName === 'custom'
setEditingApiMode({ ...editingApiMode, itemName, isCustom })
}}
>
{ModelGroups[editingApiMode.groupName].value.map((itemName) => (
<option key={itemName} value={itemName}>
{modelNameToDesc(itemName, t)}
</option>
))}
{!AlwaysCustomGroups.includes(editingApiMode.groupName) && (
<option value="custom">{t('Custom')}</option>
)}
</select>
{(editingApiMode.isCustom || AlwaysCustomGroups.includes(editingApiMode.groupName)) && (
<input
type="text"
value={editingApiMode.customName}
placeholder={t('Model Name')}
onChange={(e) => setEditingApiMode({ ...editingApiMode, customName: e.target.value })}
/>
)}
</div>
{CustomUrlGroups.includes(editingApiMode.groupName) &&
(editingApiMode.isCustom || AlwaysCustomGroups.includes(editingApiMode.groupName)) && (
<input
type="text"
value={editingApiMode.customUrl}
placeholder={t('API Url')}
onChange={(e) => setEditingApiMode({ ...editingApiMode, customUrl: e.target.value })}
/>
)}
{CustomApiKeyGroups.includes(editingApiMode.groupName) &&
(editingApiMode.isCustom || AlwaysCustomGroups.includes(editingApiMode.groupName)) && (
<input
type="password"
value={editingApiMode.apiKey}
placeholder={t('API Key')}
onChange={(e) => setEditingApiMode({ ...editingApiMode, apiKey: e.target.value })}
/>
)}
</div>
)
return (
<>
{config.apiModes.map((modelName) => {
let desc
if (modelName.includes('-')) {
const splits = modelName.split('-')
if (splits[0] in Models)
desc = `${t(Models[splits[0]].desc)} (${t(ModelMode[splits[1]])})`
} else {
if (modelName in Models) desc = t(Models[modelName].desc)
}
if (desc)
return (
<label key={modelName}>
{config.customApiModes.map(
(apiMode, index) =>
apiMode.groupName &&
apiMode.itemName &&
(editing && editingIndex === index ? (
editingComponent
) : (
<label key={index} style={{ display: 'flex', alignItems: 'center' }}>
<input
type="checkbox"
checked={config.activeApiModes.includes(modelName)}
checked={apiMode.active}
onChange={(e) => {
const checked = e.target.checked
const activeApiModes = config.activeApiModes.filter((i) => i !== modelName)
if (checked) activeApiModes.push(modelName)
updateConfig({ activeApiModes })
const customApiModes = [...config.customApiModes]
customApiModes[index] = { ...apiMode, active: e.target.checked }
updateConfig({ customApiModes })
}}
/>
{desc}
{modelNameToDesc(apiModeToModelName(apiMode), t)}
<div style={{ flexGrow: 1 }} />
<div style={{ display: 'flex', gap: '12px' }}>
<div
style={{ cursor: 'pointer' }}
onClick={(e) => {
e.preventDefault()
setEditing(true)
setEditingApiMode(apiMode)
setEditingIndex(index)
}}
>
<PencilIcon />
</div>
<div
style={{ cursor: 'pointer' }}
onClick={(e) => {
e.preventDefault()
const customApiModes = [...config.customApiModes]
customApiModes.splice(index, 1)
updateConfig({ customApiModes })
}}
>
<TrashIcon />
</div>
</div>
</label>
)
})}
)),
)}
<div style={{ height: '30px' }} />
{editing ? (
editingIndex === -1 ? (
editingComponent
) : undefined
) : (
<button
onClick={(e) => {
e.preventDefault()
setEditing(true)
setEditingApiMode(defaultApiMode)
setEditingIndex(-1)
}}
>
{t('New')}
</button>
)}
</>
)
}
+2 -10
View File
@@ -1,7 +1,7 @@
import { useTranslation } from 'react-i18next'
import { useState } from 'react'
import FileSaver from 'file-saver'
import { openUrl } from '../../utils/index.mjs'
import { openUrl, modelNameToDesc } from '../../utils/index.mjs'
import {
isUsingOpenAiApiKey,
isUsingAzureOpenAi,
@@ -13,7 +13,6 @@ import {
isUsingGithubThirdPartyApi,
isUsingMultiModeModel,
ModelMode,
Models,
ThemeMode,
TriggerMode,
isUsingMoonshotApi,
@@ -161,14 +160,7 @@ export function GeneralPart({ config, updateConfig }) {
}}
>
{config.activeApiModes.map((modelName) => {
let desc
if (modelName.includes('-')) {
const splits = modelName.split('-')
if (splits[0] in Models)
desc = `${t(Models[splits[0]].desc)} (${t(ModelMode[splits[1]])})`
} else {
if (modelName in Models) desc = t(Models[modelName].desc)
}
const desc = modelNameToDesc(modelName, t)
if (desc)
return (
<option
+1 -1
View File
@@ -62,7 +62,7 @@ export function SelectionTools({ config, updateConfig }) {
</button>
</div>
{errorMessage && <div style={{ color: 'red' }}>{errorMessage}</div>}
<div style={{ display: 'flex', gap: '4px', alignItems: 'center' }}>
<div style={{ display: 'flex', gap: '4px', alignItems: 'center', whiteSpace: 'noWrap' }}>
{t('Name')}
<input
type="text"
+2 -1
View File
@@ -1,6 +1,7 @@
import BingAIClient from '../clients/bing/index.mjs'
import { getUserConfig } from '../../config/index.mjs'
import { pushRecord, setAbortController } from './shared.mjs'
import { isCustomModelName, modelNameToCustomPart } from '../../utils/model-name-convert.mjs'
/**
* @param {Runtime.Port} port
@@ -19,7 +20,7 @@ export async function generateAnswersWithBingWebApi(
const { controller, messageListener, disconnectListener } = setAbortController(port)
const config = await getUserConfig()
let modelMode
if (session.modelName.includes('-')) modelMode = session.modelName.split('-')[1]
if (isCustomModelName(session.modelName)) modelMode = modelNameToCustomPart(session.modelName)
else modelMode = config.modelMode
console.debug('mode', modelMode)
+2 -1
View File
@@ -9,6 +9,7 @@ import { v4 as uuidv4 } from 'uuid'
import { t } from 'i18next'
import { sha3_512 } from 'js-sha3'
import randomInt from 'random-int'
import { modelNameToValue } from '../../utils/model-name-convert.mjs'
async function request(token, method, path, data) {
const apiUrl = (await getUserConfig()).customChatGptWebApiUrl
@@ -233,7 +234,7 @@ export async function generateAnswersWithChatgptWebApi(port, question, session,
isNeedWebsocket(accessToken).catch(() => undefined),
])
console.debug('models', models)
const selectedModel = Models[session.modelName].value
const selectedModel = modelNameToValue(session.modelName)
const usedModel =
models && models.includes(selectedModel) ? selectedModel : Models[chatgptWebModelKeys[0]].value
console.debug('usedModel', usedModel)
+3 -2
View File
@@ -1,8 +1,9 @@
import { getUserConfig, Models } from '../../config/index.mjs'
import { getUserConfig } from '../../config/index.mjs'
import { pushRecord, setAbortController } from './shared.mjs'
import { fetchSSE } from '../../utils/fetch-sse.mjs'
import { isEmpty } from 'lodash-es'
import { getConversationPairs } from '../../utils/get-conversation-pairs.mjs'
import { modelNameToValue } from '../../utils/model-name-convert.mjs'
/**
* @param {Runtime.Port} port
@@ -31,7 +32,7 @@ export async function generateAnswersWithClaudeApi(port, question, session) {
'x-api-key': config.claudeApiKey,
},
body: JSON.stringify({
model: Models[modelName].value,
model: modelNameToValue(modelName),
messages: prompt,
stream: true,
max_tokens: config.maxResponseTokenLength,
+2 -2
View File
@@ -1,6 +1,6 @@
import { pushRecord, setAbortController } from './shared.mjs'
import Claude from '../clients/claude'
import { Models } from '../../config/index.mjs'
import { modelNameToValue } from '../../utils/model-name-convert.mjs'
/**
* @param {Runtime.Port} port
@@ -35,7 +35,7 @@ export async function generateAnswersWithClaudeWebApi(
const params = {
progress: progressFunc,
done: doneFunc,
model: Models[modelName].value,
model: modelNameToValue(modelName),
signal: controller.signal,
}
+3 -2
View File
@@ -1,7 +1,8 @@
import { pushRecord, setAbortController } from './shared.mjs'
import { Models, setUserConfig } from '../../config/index.mjs'
import { setUserConfig } from '../../config/index.mjs'
import { fetchSSE } from '../../utils/fetch-sse'
import { isEmpty } from 'lodash-es'
import { modelNameToValue } from '../../utils/model-name-convert.mjs'
export class MoonshotWeb {
/**
@@ -596,7 +597,7 @@ export async function generateAnswersWithMoonshotWebApi(
const params = {
progress: progressFunc,
done: doneFunc,
model: Models[modelName].value,
model: modelNameToValue(modelName),
signal: controller.signal,
}
+4 -3
View File
@@ -1,6 +1,6 @@
// api version
import { Models, getUserConfig } from '../../config/index.mjs'
import { getUserConfig } from '../../config/index.mjs'
import { fetchSSE } from '../../utils/fetch-sse.mjs'
import { getConversationPairs } from '../../utils/get-conversation-pairs.mjs'
import { isEmpty } from 'lodash-es'
@@ -10,6 +10,7 @@ import {
pushRecord,
setAbortController,
} from './shared.mjs'
import { modelNameToValue } from '../../utils/model-name-convert.mjs'
/**
* @param {Browser.Runtime.Port} port
@@ -54,7 +55,7 @@ export async function generateAnswersWithGptCompletionApi(
},
body: JSON.stringify({
prompt: prompt,
model: Models[modelName].value,
model: modelNameToValue(modelName),
stream: true,
max_tokens: config.maxResponseTokenLength,
temperature: config.temperature,
@@ -154,7 +155,7 @@ export async function generateAnswersWithChatgptApiCompat(
},
body: JSON.stringify({
messages: prompt,
model: Models[modelName].value,
model: modelNameToValue(modelName),
stream: true,
max_tokens: config.maxResponseTokenLength,
temperature: config.temperature,
+2 -2
View File
@@ -1,5 +1,5 @@
import { Models } from '../config/index.mjs'
import { v4 as uuidv4 } from 'uuid'
import { modelNameToDesc } from '../utils/model-name-convert.mjs'
/**
* @typedef {object} Session
@@ -54,7 +54,7 @@ export function initSession({
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
aiName: modelName ? Models[modelName].desc : null,
aiName: modelName ? modelNameToDesc(modelName) : null,
modelName,
autoClean,
+2 -2
View File
@@ -3,11 +3,11 @@ import {
claudeWebModelKeys,
clearOldAccessToken,
getUserConfig,
Models,
setAccessToken,
} from '../config/index.mjs'
import Browser from 'webextension-polyfill'
import { t } from 'i18next'
import { modelNameToDesc } from '../utils/model-name-convert.mjs'
export async function getChatGptAccessToken() {
await clearOldAccessToken()
@@ -103,7 +103,7 @@ export function registerPortListener(executor) {
if (!session) return
const config = await getUserConfig()
if (!session.modelName) session.modelName = config.modelName
if (!session.aiName) session.aiName = Models[session.modelName].desc
if (!session.aiName) session.aiName = modelNameToDesc(session.modelName)
port.postMessage({ session })
try {
await executor(session, port, config)
+3 -2
View File
@@ -21,7 +21,8 @@
// SOFTWARE.
import { encode } from '@nem035/gpt-3-encoder'
import { getUserConfig, Models } from '../config/index.mjs'
import { getUserConfig } from '../config/index.mjs'
import { modelNameToDesc } from './model-name-convert.mjs'
const clamp = (v, min, max) => {
return Math.min(Math.max(v, min), max)
@@ -35,7 +36,7 @@ export async function cropText(
tiktoken = true,
) {
const userConfig = await getUserConfig()
const k = Models[userConfig.modelName].desc.match(/[- (]*([0-9]+)k/)?.[1]
const k = modelNameToDesc(userConfig.modelName).match(/[- (]*([0-9]+)k/)?.[1]
if (k) {
maxLength = Number(k) * 1000
maxLength -= 100 + clamp(userConfig.maxResponseTokenLength, 1, maxLength - 1000)
+1
View File
@@ -19,3 +19,4 @@ export * from './set-element-position-in-viewport'
export * from './eventsource-parser.mjs'
export * from './update-ref-height'
export * from './wait-for-element-to-exist-and-select.mjs'
export * from './model-name-convert.mjs'
+80
View File
@@ -0,0 +1,80 @@
import { AlwaysCustomGroups, ModelGroups, ModelMode, Models } from '../config/index.mjs'
export function modelNameToDesc(modelName, t) {
if (!t) t = (x) => x
if (modelName in Models) return t(Models[modelName].desc)
let desc = modelName
if (isCustomModelName(modelName)) {
const presetPart = modelNameToPresetPart(modelName)
const customPart = modelNameToCustomPart(modelName)
if (presetPart in Models) {
if (customPart in ModelMode)
desc = `${t(Models[presetPart].desc)} (${t(ModelMode[customPart])})`
else desc = `${t(Models[presetPart].desc)} (${customPart})`
} else if (presetPart in ModelGroups) {
desc = `${t(ModelGroups[presetPart].desc)} (${customPart})`
}
}
return desc
}
export function modelNameToPresetPart(modelName) {
if (isCustomModelName(modelName)) {
return modelName.split('-')[0]
} else {
return modelName
}
}
export function modelNameToCustomPart(modelName) {
if (isCustomModelName(modelName)) {
return modelName.substring(modelName.indexOf('-') + 1)
} else {
return modelName
}
}
export function modelNameToValue(modelName) {
if (modelName in Models) return Models[modelName].value
return modelNameToCustomPart(modelName)
}
export function isCustomModelName(modelName) {
return modelName.includes('-')
}
export function modelNameToApiMode(modelName) {
const presetPart = modelNameToPresetPart(modelName)
const found =
Object.entries(ModelGroups).find(([k]) => presetPart === k) ||
Object.entries(ModelGroups).find(([, g]) => presetPart in g.value)
if (found) {
const [groupName] = found
const isCustom = isCustomModelName(modelName)
let customName = ''
if (isCustom) customName = modelNameToCustomPart()
return {
groupName,
itemName: presetPart,
isCustom,
customName,
customUrl: '',
apiKey: '',
active: true,
}
}
}
export function apiModeToModelName(apiMode) {
if (AlwaysCustomGroups.includes(apiMode.groupName))
return apiMode.groupName + '-' + apiMode.customName
if (apiMode.isCustom) {
if (apiMode.itemName === 'custom') return apiMode.groupName + '-' + apiMode.customName
return apiMode.itemName + '-' + apiMode.customName
}
return apiMode.itemName
}