Add ChatGLM API (#567)

* Add ChatGLM API

* add minimal build

* a small patch

---------

Co-authored-by: josc146 <josStorer@outlook.com>
This commit is contained in:
AceLam
2023-12-03 23:31:04 +08:00
committed by GitHub
parent 70d6b794f0
commit d24958f9a0
9 changed files with 4629 additions and 2390 deletions
+44 -2
View File
@@ -18,7 +18,7 @@ async function deleteOldDir() {
await fs.rm(outdir, { recursive: true, force: true })
}
async function runWebpack(isWithoutKatex, isWithoutTiktoken, callback) {
async function runWebpack(isWithoutKatex, isWithoutTiktoken, minimal, callback) {
const shared = [
'preact',
'webextension-polyfill',
@@ -70,6 +70,12 @@ async function runWebpack(isWithoutKatex, isWithoutTiktoken, callback) {
concatenateModules: !isAnalyzing,
},
plugins: [
minimal
? undefined
: new webpack.ProvidePlugin({
process: 'process/browser.js',
Buffer: ['buffer', 'Buffer'],
}),
new ProgressBarPlugin({
format: ' build [:bar] :percent (:elapsed seconds)',
clear: false,
@@ -97,6 +103,14 @@ async function runWebpack(isWithoutKatex, isWithoutTiktoken, callback) {
extensions: ['.jsx', '.mjs', '.js'],
alias: {
parse5: path.resolve(__dirname, 'node_modules/parse5'),
...(minimal
? {}
: {
util: path.resolve(__dirname, 'node_modules/util'),
buffer: path.resolve(__dirname, 'node_modules/buffer'),
stream: 'stream-browserify',
crypto: 'crypto-browserify',
}),
},
},
module: {
@@ -206,7 +220,7 @@ async function runWebpack(isWithoutKatex, isWithoutTiktoken, callback) {
},
}
: {},
isWithoutKatex && isWithoutTiktoken
minimal
? {
test: /styles\.scss$/,
loader: 'string-replace-loader',
@@ -220,6 +234,32 @@ async function runWebpack(isWithoutKatex, isWithoutTiktoken, callback) {
},
}
: {},
minimal
? {
test: /index\.mjs$/,
loader: 'string-replace-loader',
options: {
multiple: [
{
search: 'import { generateAnswersWithChatGLMApi }',
replace: '//',
},
{
search: 'await generateAnswersWithChatGLMApi',
replace: '//',
},
{
search: 'chatglmTurbo',
replace: '//',
},
{
search: "'chatglmTurbo",
replace: '//',
},
],
},
}
: {},
],
},
})
@@ -305,6 +345,7 @@ async function build() {
// )
// await new Promise((r) => setTimeout(r, 5000))
await runWebpack(
true,
true,
true,
generateWebpackCallback(() => finishOutput('-without-katex-and-tiktoken')),
@@ -312,6 +353,7 @@ async function build() {
await new Promise((r) => setTimeout(r, 10000))
}
await runWebpack(
false,
false,
false,
generateWebpackCallback(() => finishOutput('')),
+4359 -2359
View File
File diff suppressed because it is too large Load Diff
+6
View File
@@ -22,19 +22,23 @@
"@nem035/gpt-3-encoder": "^1.1.7",
"@picocss/pico": "^1.5.9",
"@primer/octicons-react": "^18.3.0",
"buffer": "^6.0.3",
"claude-ai": "^1.2.2",
"countries-list": "^2.6.1",
"crypto-browserify": "^3.12.0",
"diff": "^5.1.0",
"file-saver": "^2.0.5",
"github-markdown-css": "^5.2.0",
"gpt-3-encoder": "^1.1.4",
"graphql": "^16.6.0",
"i18next": "^22.4.15",
"jsonwebtoken": "8.5.1",
"katex": "^0.16.6",
"lodash-es": "^4.17.21",
"md5": "^2.3.0",
"parse5": "^6.0.1",
"preact": "^10.13.2",
"process": "^0.11.10",
"prop-types": "^15.8.1",
"react": "npm:@preact/compat@^17.1.2",
"react-bootstrap-icons": "^1.10.3",
@@ -50,6 +54,8 @@
"remark-breaks": "^3.0.2",
"remark-gfm": "^3.0.1",
"remark-math": "^5.1.1",
"stream-browserify": "^3.0.0",
"util": "^0.12.5",
"uuid": "^9.0.0",
"webextension-polyfill": "^0.10.0"
},
+4
View File
@@ -12,10 +12,12 @@ import {
import { generateAnswersWithCustomApi } from '../services/apis/custom-api.mjs'
import { generateAnswersWithAzureOpenaiApi } from '../services/apis/azure-openai-api.mjs'
import { generateAnswersWithClaudeApi } from '../services/apis/claude-api.mjs'
import { generateAnswersWithChatGLMApi } from '../services/apis/chatglm-api.mjs'
import { generateAnswersWithWaylaidwandererApi } from '../services/apis/waylaidwanderer-api.mjs'
import {
azureOpenAiApiModelKeys,
claudeApiModelKeys,
chatglmApiModelKeys,
bardWebModelKeys,
bingWebModelKeys,
chatgptApiModelKeys,
@@ -122,6 +124,8 @@ async function executeApi(session, port, config) {
await generateAnswersWithAzureOpenaiApi(port, session.question, session)
} else if (claudeApiModelKeys.includes(session.modelName)) {
await generateAnswersWithClaudeApi(port, session.question, session)
} else if (chatglmApiModelKeys.includes(session.modelName)) {
await generateAnswersWithChatGLMApi(port, session.question, session, session.modelName)
} else if (githubThirdPartyApiModelKeys.includes(session.modelName)) {
await generateAnswersWithWaylaidwandererApi(port, session.question, session)
} else if (poeWebModelKeys.includes(session.modelName)) {
+37 -28
View File
@@ -45,6 +45,7 @@ export const chatgptApiModelKeys = [
export const customApiModelKeys = ['customModel']
export const azureOpenAiApiModelKeys = ['azureOpenAi']
export const claudeApiModelKeys = ['claude2Api']
export const chatglmApiModelKeys = ['chatglmTurbo']
export const githubThirdPartyApiModelKeys = ['waylaidwandererApi']
export const poeWebModelKeys = [
'poeAiWebSage', //poe.com/Assistant
@@ -72,18 +73,43 @@ export const poeWebModelKeys = [
*/
export const Models = {
chatgptFree35: { value: 'text-davinci-002-render-sha', desc: 'ChatGPT (Web)' },
chatgptFree35Mobile: { value: 'text-davinci-002-render-sha-mobile', desc: 'ChatGPT (Mobile)' },
chatgptPlus4: { value: 'gpt-4', desc: 'ChatGPT (Web, GPT-4)' },
chatgptPlus4Browsing: { value: 'gpt-4-browsing', desc: 'ChatGPT (Web, GPT-4, Browsing)' },
chatgptPlus4Mobile: { value: 'gpt-4-mobile', desc: 'ChatGPT (Mobile, GPT-4)' },
chatgptApi35: { value: 'gpt-3.5-turbo', desc: 'ChatGPT (GPT-3.5-turbo)' },
chatgptApi35_16k: { value: 'gpt-3.5-turbo-16k', desc: 'ChatGPT (GPT-3.5-turbo-16k)' },
chatgptApi35_1106: { value: 'gpt-3.5-turbo-1106', desc: 'ChatGPT (GPT-3.5-turbo 1106)' },
chatgptApi4_8k: { value: 'gpt-4', desc: 'ChatGPT (GPT-4-8k)' },
chatgptApi4_32k: { value: 'gpt-4-32k', desc: 'ChatGPT (GPT-4-32k)' },
chatgptApi4_128k_preview: {
value: 'gpt-4-1106-preview',
desc: 'ChatGPT (GPT-4-Turbo 128k Preview)',
},
claude2WebFree: { value: 'claude-2', desc: 'Claude.ai (Web, Claude 2)' },
claude2Api: { value: '', desc: 'Claude.ai (API, Claude 2)' },
bingFree4: { value: '', desc: 'Bing (Web, GPT-4)' },
bingFreeSydney: { value: '', desc: 'Bing (Web, GPT-4, Sydney)' },
bardWebFree: { value: '', desc: 'Bard (Web)' },
chatglmTurbo: { value: 'chatglm_turbo', desc: 'ChatGLM (ChatGLM-Turbo)' },
chatgptFree35Mobile: { value: 'text-davinci-002-render-sha-mobile', desc: 'ChatGPT (Mobile)' },
chatgptPlus4Mobile: { value: 'gpt-4-mobile', desc: 'ChatGPT (Mobile, GPT-4)' },
chatgptApi35_1106: { value: 'gpt-3.5-turbo-1106', desc: 'ChatGPT (GPT-3.5-turbo 1106)' },
chatgptApi4_8k_0613: { value: 'gpt-4', desc: 'ChatGPT (GPT-4-8k 0613)' },
chatgptApi4_32k_0613: { value: 'gpt-4-32k', desc: 'ChatGPT (GPT-4-32k 0613)' },
gptApiDavinci: { value: 'text-davinci-003', desc: 'GPT-3.5' },
customModel: { value: '', desc: 'Custom Model' },
azureOpenAi: { value: '', desc: 'ChatGPT (Azure)' },
waylaidwandererApi: { value: '', desc: 'Waylaidwanderer API (Github)' },
poeAiWebSage: { value: 'Assistant', desc: 'Poe AI (Web, Assistant)' },
poeAiWebGPT4: { value: 'gpt-4', desc: 'Poe AI (Web, GPT-4)' },
poeAiWebGPT4_32k: { value: 'gpt-4-32k', desc: 'Poe AI (Web, GPT-4-32k)' },
@@ -94,21 +120,9 @@ export const Models = {
poeAiWeb_Llama_2_7b: { value: 'Llama-2-7b', desc: 'Poe AI (Web, Llama-2-7b)' },
poeAiWeb_Llama_2_13b: { value: 'Llama-2-13b', desc: 'Poe AI (Web, Llama-2-13b)' },
poeAiWeb_Llama_2_70b: { value: 'Llama-2-70b', desc: 'Poe AI (Web, Llama-2-70b)' },
chatgptApi4_8k: { value: 'gpt-4', desc: 'ChatGPT (GPT-4-8k)' },
chatgptApi4_8k_0613: { value: 'gpt-4', desc: 'ChatGPT (GPT-4-8k 0613)' },
chatgptApi4_32k: { value: 'gpt-4-32k', desc: 'ChatGPT (GPT-4-32k)' },
chatgptApi4_32k_0613: { value: 'gpt-4-32k', desc: 'ChatGPT (GPT-4-32k 0613)' },
chatgptApi4_128k_preview: {
value: 'gpt-4-1106-preview',
desc: 'ChatGPT (GPT-4-Turbo 128k Preview)',
},
gptApiDavinci: { value: 'text-davinci-003', desc: 'GPT-3.5' },
customModel: { value: '', desc: 'Custom Model' },
azureOpenAi: { value: '', desc: 'ChatGPT (Azure)' },
waylaidwandererApi: { value: '', desc: 'Waylaidwanderer API (Github)' },
poeAiWebCustom: { value: '', desc: 'Poe AI (Web, Custom)' },
poeAiWebChatGpt: { value: 'chatgpt', desc: 'Poe AI (Web, ChatGPT)' },
poeAiWebChatGpt_16k: { value: 'chatgpt-16k', desc: 'Poe AI (Web, ChatGPT-16k)' },
poeAiWebCustom: { value: '', desc: 'Poe AI (Web, Custom)' },
}
for (const modelName in Models) {
@@ -152,6 +166,7 @@ export const defaultConfig = {
poeCustomBotName: '',
claudeApiKey: '',
chatglmApiKey: '',
customApiKey: '',
@@ -181,25 +196,15 @@ export const defaultConfig = {
alwaysCreateNewConversationWindow: false,
activeApiModes: [
// 'claude2Api',
'chatgptFree35',
//'chatgptFree35Mobile',
'chatgptPlus4',
// 'chatgptPlus4Mobile',
'chatgptApi35',
'chatgptApi35_16k',
'chatgptApi4_8k',
'claude2WebFree',
'bingFree4',
'bingFreeSydney',
// 'poeAiWebSage', //poe.com/Assistant
// 'poeAiWebGPT4',
// 'poeAiWebGPT4_32k',
// 'poeAiWebClaudePlus',
// 'poeAiWebClaude100k',
'chatgptApi4_8k',
'chatglmTurbo',
'customModel',
'azureOpenAi',
// 'poeAiWebCustom',
],
activeSelectionTools: ['translate', 'summary', 'polish', 'code', 'ask'],
activeSiteAdapters: [
@@ -290,6 +295,10 @@ export function isUsingCustomModel(configOrSession) {
return customApiModelKeys.includes(configOrSession.modelName)
}
export function isUsingChatGLMApi(configOrSession) {
return chatglmApiModelKeys.includes(configOrSession.modelName)
}
export function isUsingCustomNameOnlyModel(configOrSession) {
return configOrSession.modelName === 'poeAiWebCustom'
}
+13
View File
@@ -4,6 +4,7 @@ import { openUrl } from '../../utils/index.mjs'
import {
isUsingApiKey,
isUsingAzureOpenAi,
isUsingChatGLMApi,
isUsingClaude2Api,
isUsingCustomModel,
isUsingCustomNameOnlyModel,
@@ -273,6 +274,18 @@ export function GeneralPart({ config, updateConfig }) {
}}
/>
)}
{isUsingChatGLMApi(config) && (
<input
type="password"
style="width: 50%;"
value={config.chatglmApiKey}
placeholder={t('ChatGLM API Key')}
onChange={(e) => {
const apiKey = e.target.value
updateConfig({ chatglmApiKey: apiKey })
}}
/>
)}
</span>
{isUsingCustomModel(config) && (
<input
+115
View File
@@ -0,0 +1,115 @@
import { Models, getUserConfig } from '../../config/index.mjs'
import { pushRecord, setAbortController } from './shared.mjs'
import { isEmpty } from 'lodash-es'
import { getToken } from '../../utils/jwt-token-generator.mjs'
import { createParser } from '../../utils/eventsource-parser.mjs'
async function fetchSSE(resource, options) {
const { onMessage, onStart, onEnd, onError, ...fetchOptions } = options
const resp = await fetch(resource, fetchOptions).catch(async (err) => {
await onError(err)
})
if (!resp) return
if (!resp.ok) {
await onError(resp)
return
}
const parser = createParser((event) => {
if (event.type === 'event') {
onMessage(event)
}
})
let hasStarted = false
const reader = resp.body.getReader()
let result
while (!(result = await reader.read()).done) {
const chunk = result.value
if (!hasStarted) {
hasStarted = true
await onStart(new TextDecoder().decode(chunk))
}
parser.feed(chunk)
}
await onEnd()
}
/**
* @param {Runtime.Port} port
* @param {string} question
* @param {Session} session
* @param {string} modelName
*/
export async function generateAnswersWithChatGLMApi(port, question, session, modelName) {
const { controller, messageListener, disconnectListener } = setAbortController(port)
const config = await getUserConfig()
const prompt = []
for (const record of session.conversationRecords.slice(-config.maxConversationContextLength)) {
prompt.push({ role: 'user', content: record.question })
prompt.push({ role: 'assistant', content: record.answer })
}
prompt.push({ role: 'user', content: question })
let answer = ''
await fetchSSE(
`https://open.bigmodel.cn/api/paas/v3/model-api/${Models[modelName].value}/sse-invoke`,
{
method: 'POST',
signal: controller.signal,
headers: {
'Content-Type': 'application/json; charset=UTF-8',
Accept: 'text/event-stream',
Authorization: getToken(config.chatglmApiKey),
},
body: JSON.stringify({
prompt: prompt,
// temperature: config.temperature,
// top_t: 0.7,
// request_id: string
// incremental: true,
// return_type: "json_string",
// ref: {"enable": "true", "search_query": "history"},
}),
onMessage(event) {
console.debug('sse event', event)
// Handle different types of events
switch (event.event) {
case 'add':
// In the case of an "add" event, append the completion to the answer
if (event.data) {
answer += event.data
port.postMessage({ answer: answer, done: false, session: null })
}
break
case 'error':
case 'interrupted':
case 'finish':
pushRecord(session, question, answer)
console.debug('conversation history', { content: session.conversationRecords })
port.postMessage({ answer: null, done: true, session: session })
break
default:
break
}
},
async onStart() {},
async onEnd() {
port.postMessage({ done: true })
port.onMessage.removeListener(messageListener)
port.onDisconnect.removeListener(disconnectListener)
},
async onError(resp) {
port.onMessage.removeListener(messageListener)
port.onDisconnect.removeListener(disconnectListener)
if (resp instanceof Error) throw resp
const error = await resp.json().catch(() => ({}))
throw new Error(
!isEmpty(error) ? JSON.stringify(error) : `${resp.status} ${resp.statusText}`,
)
},
},
)
}
+8 -1
View File
@@ -9,6 +9,7 @@ function createParser(onParse) {
let eventId
let eventName
let data
let extra
reset()
return {
feed,
@@ -78,17 +79,19 @@ function createParser(onParse) {
function parseEventStreamLine(lineBuffer, index, fieldLength, lineLength) {
if (lineLength === 0) {
if (data.length > 0) {
if (data.length > 0 || extra) {
onParse({
type: 'event',
id: eventId,
event: eventName || void 0,
data: data.slice(0, -1),
extra: extra || void 0,
// remove trailing newline
})
data = ''
eventId = void 0
extra = void 0
}
eventName = void 0
return
@@ -120,6 +123,10 @@ function createParser(onParse) {
value: retry,
})
}
} else {
const str = `{"${field}":${value}}`
extra = extra ?? []
extra.push(JSON.parse(str))
}
}
}
+43
View File
@@ -0,0 +1,43 @@
import jwt from 'jsonwebtoken'
let jwtToken = null
let tokenExpiration = null // Declare tokenExpiration in the module scope
function generateToken(apiKey, timeoutSeconds) {
const parts = apiKey.split('.')
if (parts.length !== 2) {
throw new Error('Invalid API key')
}
const ms = Date.now()
const currentSeconds = Math.floor(ms / 1000)
const [id, secret] = parts
const payload = {
api_key: id,
exp: currentSeconds + timeoutSeconds,
timestamp: currentSeconds,
}
jwtToken = jwt.sign(payload, secret, {
header: {
alg: 'HS256',
typ: 'JWT',
sign_type: 'SIGN',
},
})
tokenExpiration = ms + timeoutSeconds * 1000
}
function shouldRegenerateToken() {
const ms = Date.now()
return !jwtToken || ms >= tokenExpiration
}
function getToken(apiKey) {
if (shouldRegenerateToken()) {
generateToken(apiKey, 86400) // Hard-coded to regenerate the token every 24 hours
}
return jwtToken
}
export { getToken }