WIP, custom API Modes

This commit is contained in:
josc146
2024-08-07 17:13:27 +08:00
parent fbb74b65d1
commit 3d89ddd5bf
15 changed files with 112 additions and 82 deletions
+10 -7
View File
@@ -11,6 +11,7 @@ import {
isFirefox,
isMobile,
isSafari,
isUsingModelName,
modelNameToDesc,
} from '../../utils'
import {
@@ -25,7 +26,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, Models } from '../../config/index.mjs'
import { getUserConfig, isUsingBingWebModel, Models } from '../../config/index.mjs'
import { useTranslation } from 'react-i18next'
import DeleteButton from '../DeleteButton'
import { useConfig } from '../../hooks/use-config.mjs'
@@ -61,8 +62,7 @@ function ConversationCard(props) {
const windowSize = useClampWindowSize([750, 1500], [250, 1100])
const bodyRef = useRef(null)
const [completeDraggable, setCompleteDraggable] = useState(false)
// `.some` for multi mode models. e.g. bingFree4-balanced
const useForegroundFetch = bingWebModelKeys.some((n) => session.modelName.includes(n))
const useForegroundFetch = isUsingBingWebModel(session)
const [apiModes, setApiModes] = useState([])
/**
@@ -247,7 +247,7 @@ function ConversationCard(props) {
}
try {
const bingToken = (await getUserConfig()).bingAccessToken
if (session.modelName.includes('bingFreeSydney'))
if (isUsingModelName('bingFreeSydney', session))
await generateAnswersWithBingWebApi(
fakePort,
session.question,
@@ -390,7 +390,11 @@ function ConversationCard(props) {
...session,
modelName,
apiMode,
aiName: modelNameToDesc(apiMode ? apiModeToModelName(apiMode) : modelName, t),
aiName: modelNameToDesc(
apiMode ? apiModeToModelName(apiMode) : modelName,
t,
config.customModelName,
),
}
if (config.autoRegenAfterSwitchModel && conversationItemData.length > 0)
getRetryFn(newSession)()
@@ -399,7 +403,7 @@ function ConversationCard(props) {
>
{apiModes.map((apiMode, index) => {
const modelName = apiModeToModelName(apiMode)
const desc = modelNameToDesc(modelName, t)
const desc = modelNameToDesc(modelName, t, config.customModelName)
if (desc) {
return (
<option value={index} key={index} selected={isApiModeSelected(apiMode, session)}>
@@ -551,7 +555,6 @@ function ConversationCard(props) {
key={idx}
type={data.type}
descName={data.type === 'answer' && session.aiName}
modelName={data.type === 'answer' && session.modelName}
onRetry={idx === conversationItemData.length - 1 ? retryFn : null}
/>
))}
+6 -19
View File
@@ -5,30 +5,18 @@ import ReadButton from '../ReadButton'
import PropTypes from 'prop-types'
import MarkdownRender from '../MarkdownRender/markdown.jsx'
import { useTranslation } from 'react-i18next'
import { isUsingCustomModel } from '../../config/index.mjs'
import { useConfig } from '../../hooks/use-config.mjs'
function AnswerTitle({ descName, modelName }) {
function AnswerTitle({ descName }) {
const { t } = useTranslation()
const config = useConfig()
return (
<p style="white-space: nowrap;">
{descName && modelName
? `${t(descName)}${
isUsingCustomModel({ modelName }) ? ' (' + config.customModelName + ')' : ''
}:`
: t('Loading...')}
</p>
)
return <p style="white-space: nowrap;">{descName ? `${descName}:` : t('Loading...')}</p>
}
AnswerTitle.propTypes = {
descName: PropTypes.string,
modelName: PropTypes.string,
}
export function ConversationItem({ type, content, descName, modelName, onRetry }) {
export function ConversationItem({ type, content, descName, onRetry }) {
const { t } = useTranslation()
const [collapsed, setCollapsed] = useState(false)
@@ -67,17 +55,17 @@ export function ConversationItem({ type, content, descName, modelName, onRetry }
return (
<div className={'chatgptbox-' + type} dir="auto">
<div className="gpt-header">
<AnswerTitle descName={descName} modelName={modelName} />
<AnswerTitle descName={descName} />
<div className="gpt-util-group">
{onRetry && (
<span title={t('Retry')} className="gpt-util-icon" onClick={onRetry}>
<SyncIcon size={14} />
</span>
)}
{modelName && (
{descName && (
<CopyButton contentFn={() => content.replace(/\n<hr\/>$/, '')} size={14} />
)}
{modelName && <ReadButton contentFn={() => content} size={14} />}
{descName && <ReadButton contentFn={() => content} size={14} />}
{!collapsed ? (
<span
title={t('Collapse')}
@@ -141,7 +129,6 @@ ConversationItem.propTypes = {
type: PropTypes.oneOf(['question', 'answer', 'error']).isRequired,
content: PropTypes.string.isRequired,
descName: PropTypes.string,
modelName: PropTypes.string,
onRetry: PropTypes.func,
}
+6 -2
View File
@@ -1,7 +1,11 @@
import { defaults } from 'lodash-es'
import Browser from 'webextension-polyfill'
import { isMobile } from '../utils/is-mobile.mjs'
import { isInApiModeGroup, modelNameToDesc } from '../utils/model-name-convert.mjs'
import {
isInApiModeGroup,
isUsingModelName,
modelNameToDesc,
} from '../utils/model-name-convert.mjs'
import { t } from 'i18next'
export const TriggerMode = {
@@ -526,7 +530,7 @@ export function isUsingCustomModel(configOrSession) {
* @deprecated
*/
export function isUsingCustomNameOnlyModel(configOrSession) {
return configOrSession.modelName === 'poeAiWebCustom'
return isUsingModelName('poeAiWebCustom', configOrSession)
}
export async function getPreferredLanguageKey() {
+22 -5
View File
@@ -9,6 +9,7 @@ import {
chatgptWebModelKeys,
getPreferredLanguageKey,
getUserConfig,
isUsingChatgptWebModel,
setAccessToken,
setUserConfig,
} from '../config/index.mjs'
@@ -91,7 +92,11 @@ async function mountComponent(siteConfig) {
render(
<FloatingToolbar
session={initSession({ modelName: userConfig.modelName, apiMode: userConfig.apiMode })}
session={initSession({
modelName: userConfig.modelName,
apiMode: userConfig.apiMode,
extraCustomModelName: userConfig.customModelName,
})}
selection=""
container={toolbarContainer}
triggered={triggered}
@@ -107,7 +112,11 @@ async function mountComponent(siteConfig) {
container.id = 'chatgptbox-container'
render(
<DecisionCard
session={initSession({ modelName: userConfig.modelName, apiMode: userConfig.apiMode })}
session={initSession({
modelName: userConfig.modelName,
apiMode: userConfig.apiMode,
extraCustomModelName: userConfig.customModelName,
})}
question={question}
siteConfig={siteConfig}
container={container}
@@ -154,7 +163,11 @@ const createSelectionTools = async (toolbarContainer, selection) => {
const userConfig = await getUserConfig()
render(
<FloatingToolbar
session={initSession({ modelName: userConfig.modelName, apiMode: userConfig.apiMode })}
session={initSession({
modelName: userConfig.modelName,
apiMode: userConfig.apiMode,
extraCustomModelName: userConfig.customModelName,
})}
selection={selection}
container={toolbarContainer}
dockable={true}
@@ -280,7 +293,11 @@ async function prepareForRightClickMenu() {
const userConfig = await getUserConfig()
render(
<FloatingToolbar
session={initSession({ modelName: userConfig.modelName, apiMode: userConfig.apiMode })}
session={initSession({
modelName: userConfig.modelName,
apiMode: userConfig.apiMode,
extraCustomModelName: userConfig.customModelName,
})}
selection={data.selectionText}
container={container}
triggered={true}
@@ -382,7 +399,7 @@ async function prepareForForegroundRequests() {
})
registerPortListener(async (session, port) => {
if (chatgptWebModelKeys.includes(session.modelName)) {
if (isUsingChatgptWebModel(session)) {
const accessToken = await getChatGptAccessToken()
await generateAnswersWithChatgptWebApi(port, session.question, session, accessToken)
}
+3 -4
View File
@@ -1,7 +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'
import { getModelValue } from '../../utils/model-name-convert.mjs'
/**
* @param {Runtime.Port} port
@@ -19,9 +19,8 @@ export async function generateAnswersWithBingWebApi(
) {
const { controller, messageListener, disconnectListener } = setAbortController(port)
const config = await getUserConfig()
let modelMode
if (isCustomModelName(session.modelName)) modelMode = modelNameToCustomPart(session.modelName)
else modelMode = config.modelMode
let modelMode = getModelValue(session)
if (!modelMode) modelMode = config.modelMode
console.debug('mode', modelMode)
+2 -2
View File
@@ -9,7 +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'
import { getModelValue } from '../../utils/model-name-convert.mjs'
async function request(token, method, path, data) {
const apiUrl = (await getUserConfig()).customChatGptWebApiUrl
@@ -234,7 +234,7 @@ export async function generateAnswersWithChatgptWebApi(port, question, session,
isNeedWebsocket(accessToken).catch(() => undefined),
])
console.debug('models', models)
const selectedModel = modelNameToValue(session.modelName)
const selectedModel = getModelValue(session)
const usedModel =
models && models.includes(selectedModel) ? selectedModel : Models.chatgptFree35.value
console.debug('usedModel', usedModel)
+3 -3
View File
@@ -3,7 +3,7 @@ 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'
import { getModelValue } from '../../utils/model-name-convert.mjs'
/**
* @param {Runtime.Port} port
@@ -14,7 +14,7 @@ export async function generateAnswersWithClaudeApi(port, question, session) {
const { controller, messageListener, disconnectListener } = setAbortController(port)
const config = await getUserConfig()
const apiUrl = config.customClaudeApiUrl
const modelName = session.modelName
const model = getModelValue(session)
const prompt = getConversationPairs(
session.conversationRecords.slice(-config.maxConversationContextLength),
@@ -32,7 +32,7 @@ export async function generateAnswersWithClaudeApi(port, question, session) {
'x-api-key': config.claudeApiKey,
},
body: JSON.stringify({
model: modelNameToValue(modelName),
model,
messages: prompt,
stream: true,
max_tokens: config.maxResponseTokenLength,
+4 -10
View File
@@ -1,24 +1,18 @@
import { pushRecord, setAbortController } from './shared.mjs'
import Claude from '../clients/claude'
import { modelNameToValue } from '../../utils/model-name-convert.mjs'
import { getModelValue } from '../../utils/model-name-convert.mjs'
/**
* @param {Runtime.Port} port
* @param {string} question
* @param {Session} session
* @param {string} sessionKey
* @param {string} modelName
*/
export async function generateAnswersWithClaudeWebApi(
port,
question,
session,
sessionKey,
modelName,
) {
export async function generateAnswersWithClaudeWebApi(port, question, session, sessionKey) {
const bot = new Claude({ sessionKey })
await bot.init()
const { controller, cleanController } = setAbortController(port)
const model = getModelValue(session)
let answer = ''
const progressFunc = ({ completion }) => {
@@ -35,7 +29,7 @@ export async function generateAnswersWithClaudeWebApi(
const params = {
progress: progressFunc,
done: doneFunc,
model: modelNameToValue(modelName),
model,
signal: controller.signal,
}
+4 -10
View File
@@ -2,7 +2,7 @@ import { pushRecord, setAbortController } from './shared.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'
import { getModelValue } from '../../utils/model-name-convert.mjs'
export class MoonshotWeb {
/**
@@ -569,18 +569,12 @@ export class Message {
* @param {string} question
* @param {Session} session
* @param {UserConfig} config
* @param {string} modelName
*/
export async function generateAnswersWithMoonshotWebApi(
port,
question,
session,
config,
modelName,
) {
export async function generateAnswersWithMoonshotWebApi(port, question, session, config) {
const bot = new MoonshotWeb({ config })
await bot.init()
const { controller, cleanController } = setAbortController(port)
const model = getModelValue(session)
let answer = ''
const progressFunc = ({ completion }) => {
@@ -597,7 +591,7 @@ export async function generateAnswersWithMoonshotWebApi(
const params = {
progress: progressFunc,
done: doneFunc,
model: modelNameToValue(modelName),
model,
signal: controller.signal,
}
+6 -11
View File
@@ -10,23 +10,17 @@ import {
pushRecord,
setAbortController,
} from './shared.mjs'
import { modelNameToValue } from '../../utils/model-name-convert.mjs'
import { getModelValue } from '../../utils/model-name-convert.mjs'
/**
* @param {Browser.Runtime.Port} port
* @param {string} question
* @param {Session} session
* @param {string} apiKey
* @param {string} modelName
*/
export async function generateAnswersWithGptCompletionApi(
port,
question,
session,
apiKey,
modelName,
) {
export async function generateAnswersWithGptCompletionApi(port, question, session, apiKey) {
const { controller, messageListener, disconnectListener } = setAbortController(port)
const model = getModelValue(session)
const config = await getUserConfig()
const prompt =
@@ -55,7 +49,7 @@ export async function generateAnswersWithGptCompletionApi(
},
body: JSON.stringify({
prompt: prompt,
model: modelNameToValue(modelName),
model,
stream: true,
max_tokens: config.maxResponseTokenLength,
temperature: config.temperature,
@@ -129,6 +123,7 @@ export async function generateAnswersWithChatgptApiCompat(
extraBody = {},
) {
const { controller, messageListener, disconnectListener } = setAbortController(port)
const model = getModelValue(session)
const config = await getUserConfig()
const prompt = getConversationPairs(
@@ -155,7 +150,7 @@ export async function generateAnswersWithChatgptApiCompat(
},
body: JSON.stringify({
messages: prompt,
model: modelNameToValue(modelName),
model,
stream: true,
max_tokens: config.maxResponseTokenLength,
temperature: config.temperature,
+7 -1
View File
@@ -37,6 +37,7 @@ import { t } from 'i18next'
* @param {string|null} modelName
* @param {boolean|null} autoClean
* @param {Object|null} apiMode
* @param {string} extraCustomModelName
* @returns {Session}
*/
export function initSession({
@@ -46,6 +47,7 @@ export function initSession({
modelName = null,
autoClean = false,
apiMode = null,
extraCustomModelName = '',
} = {}) {
return {
// common
@@ -59,7 +61,11 @@ export function initSession({
aiName:
modelName || apiMode
? modelNameToDesc(apiMode ? apiModeToModelName(apiMode) : modelName, t)
? modelNameToDesc(
apiMode ? apiModeToModelName(apiMode) : modelName,
t,
extraCustomModelName,
)
: null,
modelName,
apiMode,
+1
View File
@@ -9,6 +9,7 @@ export const initDefaultSession = async () => {
modelName: config.modelName,
apiMode: config.apiMode,
autoClean: false,
extraCustomModelName: config.customModelName,
})
}
+6 -6
View File
@@ -1,8 +1,8 @@
import {
bingWebModelKeys,
claudeWebModelKeys,
clearOldAccessToken,
getUserConfig,
isUsingBingWebModel,
isUsingClaudeWebModel,
setAccessToken,
} from '../config/index.mjs'
import Browser from 'webextension-polyfill'
@@ -70,15 +70,14 @@ export function handlePortError(session, port, err) {
else if (['authentication token has expired'].some((m) => err.message.includes(m)))
port.postMessage({ error: 'UNAUTHORIZED' })
else if (
claudeWebModelKeys.includes(session.modelName) &&
isUsingClaudeWebModel(session) &&
['Invalid authorization', 'Session key required'].some((m) => err.message.includes(m))
)
port.postMessage({
error: t('Please login at https://claude.ai first, and then click the retry button'),
})
else if (
// `.some` for multi mode models. e.g. bingFree4-balanced
bingWebModelKeys.some((n) => session.modelName.includes(n)) &&
isUsingBingWebModel(session) &&
['/turing/conversation/create: failed to parse response body.'].some((m) =>
err.message.includes(m),
)
@@ -88,7 +87,7 @@ export function handlePortError(session, port, err) {
}
} else {
const errMsg = JSON.stringify(err)
if (bingWebModelKeys.some((n) => session.modelName.includes(n)) && errMsg.includes('isTrusted'))
if (isUsingBingWebModel(session) && errMsg.includes('isTrusted'))
port.postMessage({ error: t('Please login at https://bing.com first') })
else port.postMessage({ error: errMsg ?? 'unknown error' })
}
@@ -108,6 +107,7 @@ export function registerPortListener(executor) {
session.aiName = modelNameToDesc(
session.apiMode ? apiModeToModelName(session.apiMode) : session.modelName,
t,
config.customModelName,
)
port.postMessage({ session })
try {
+1
View File
@@ -38,6 +38,7 @@ export async function cropText(
const userConfig = await getUserConfig()
const k = modelNameToDesc(
userConfig.apiMode ? apiModeToModelName(userConfig.apiMode) : userConfig.modelName,
userConfig.customModelName,
).match(/[- (]*([0-9]+)k/)?.[1]
if (k) {
maxLength = Number(k) * 1000
+31 -2
View File
@@ -1,8 +1,13 @@
import { AlwaysCustomGroups, ModelGroups, ModelMode, Models } from '../config/index.mjs'
export function modelNameToDesc(modelName, t) {
export function modelNameToDesc(modelName, t, extraCustomModelName = '') {
if (!t) t = (x) => x
if (modelName in Models) return t(Models[modelName].desc)
if (modelName in Models) {
const desc = t(Models[modelName].desc)
if (modelName === 'customModel' && extraCustomModelName)
return `${desc} (${extraCustomModelName})`
return desc
}
let desc = modelName
if (isCustomModelName(modelName)) {
@@ -41,6 +46,13 @@ export function modelNameToValue(modelName) {
return modelNameToCustomPart(modelName)
}
export function getModelValue(configOrSession) {
let value
if (configOrSession.apiMode) value = modelNameToValue(apiModeToModelName(configOrSession.apiMode))
else value = modelNameToValue(configOrSession.modelName)
return value
}
export function isCustomModelName(modelName) {
return modelName ? modelName.includes('-') : false
}
@@ -113,6 +125,23 @@ export function isApiModeSelected(apiMode, configOrSession) {
: configOrSession.modelName === apiModeToModelName(apiMode)
}
// also match custom modelName, e.g. when modelName is bingFree4, configOrSession model is bingFree4-fast, it returns true
export function isUsingModelName(modelName, configOrSession) {
let configOrSessionModelName = configOrSession.apiMode
? apiModeToModelName(configOrSession.apiMode)
: configOrSession.modelName
if (modelName === configOrSessionModelName) {
return true
}
if (isCustomModelName(configOrSessionModelName)) {
const presetPart = modelNameToPresetPart(configOrSessionModelName)
if (presetPart in Models) configOrSessionModelName = presetPart
else if (presetPart in ModelGroups) configOrSessionModelName = ModelGroups[presetPart].value[0]
}
return configOrSessionModelName === modelName
}
export function getModelNameGroup(modelName) {
const presetPart = modelNameToPresetPart(modelName)
return (