Skip to content

Commit 4881700

Browse files
authored
Add Ollama native API to support keep alive parameters (#748)
* Add Chinese translation. * Optimize style. * Add Ollama native API to support keep alive parameters. * Optimized popup page style. * fix: Fixed data type for Ollama keep_alive parameter forever
1 parent 9ad6698 commit 4881700

File tree

13 files changed

+241
-5
lines changed

13 files changed

+241
-5
lines changed

src/_locales/zh-hans/main.json

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@
7777
"ChatGPT (GPT-4-8k)": "ChatGPT (GPT-4-8k)",
7878
"ChatGPT (GPT-4-32k)": "ChatGPT (GPT-4-32k)",
7979
"GPT-3.5": "GPT-3.5",
80+
"Ollama API": "Ollama API",
8081
"Custom Model": "自定义模型",
8182
"Balanced": "平衡",
8283
"Creative": "有创造力",
@@ -142,5 +143,13 @@
142143
"Icon": "图标",
143144
"Prompt Template": "提示模板",
144145
"Explain this: {{selection}}": "解释这个: {{selection}}",
145-
"New": "新建"
146+
"New": "新建",
147+
"DisplayMode": "显示方式",
148+
"Display in sidebar": "在侧边栏显示",
149+
"Display in floating toolbar": "在浮动工具栏显示",
150+
"Temperature": "温度",
151+
"keep-alive Time": "保活时间",
152+
"5m": "5分钟",
153+
"30m": "半小时",
154+
"Forever": "永久"
146155
}

src/background/index.mjs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
generateAnswersWithGptCompletionApi,
1111
} from '../services/apis/openai-api'
1212
import { generateAnswersWithCustomApi } from '../services/apis/custom-api.mjs'
13+
import { generateAnswersWithOllamaApi } from '../services/apis/ollama-api.mjs'
1314
import { generateAnswersWithAzureOpenaiApi } from '../services/apis/azure-openai-api.mjs'
1415
import { generateAnswersWithClaudeApi } from '../services/apis/claude-api.mjs'
1516
import { generateAnswersWithChatGLMApi } from '../services/apis/chatglm-api.mjs'
@@ -25,6 +26,7 @@ import {
2526
claudeWebModelKeys,
2627
moonshotWebModelKeys,
2728
customApiModelKeys,
29+
ollamaApiModelKeys,
2830
defaultConfig,
2931
getUserConfig,
3032
githubThirdPartyApiModelKeys,
@@ -124,6 +126,14 @@ async function executeApi(session, port, config) {
124126
config.customApiKey,
125127
config.customModelName,
126128
)
129+
} else if (ollamaApiModelKeys.includes(session.modelName)) {
130+
await generateAnswersWithOllamaApi(
131+
port,
132+
session.question,
133+
session,
134+
config.ollamaApiKey,
135+
config.ollamaModelName,
136+
)
127137
} else if (azureOpenAiApiModelKeys.includes(session.modelName)) {
128138
await generateAnswersWithAzureOpenaiApi(port, session.question, session)
129139
} else if (claudeApiModelKeys.includes(session.modelName)) {

src/components/ConversationItem/index.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import ReadButton from '../ReadButton'
55
import PropTypes from 'prop-types'
66
import MarkdownRender from '../MarkdownRender/markdown.jsx'
77
import { useTranslation } from 'react-i18next'
8-
import { isUsingCustomModel } from '../../config/index.mjs'
8+
import { isUsingCustomModel, isUsingOllamaModel } from '../../config/index.mjs'
99
import { useConfig } from '../../hooks/use-config.mjs'
1010

1111
function AnswerTitle({ descName, modelName }) {

src/config/index.mjs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ export const chatgptApiModelKeys = [
5757
'chatgptApi4_128k_0125_preview',
5858
]
5959
export const customApiModelKeys = ['customModel']
60+
export const ollamaApiModelKeys = ['ollamaModel']
6061
export const azureOpenAiApiModelKeys = ['azureOpenAi']
6162
export const claudeApiModelKeys = [
6263
'claude12Api',
@@ -163,6 +164,7 @@ export const Models = {
163164
gptApiDavinci: { value: 'text-davinci-003', desc: 'GPT-3.5' },
164165

165166
customModel: { value: '', desc: 'Custom Model' },
167+
ollamaModel: { value: '', desc: 'Ollama API' },
166168
azureOpenAi: { value: '', desc: 'ChatGPT (Azure)' },
167169
waylaidwandererApi: { value: '', desc: 'Waylaidwanderer API (Github)' },
168170

@@ -249,6 +251,10 @@ export const defaultConfig = {
249251
customModelName: 'gpt-3.5-turbo',
250252
githubThirdPartyUrl: 'http://127.0.0.1:3000/conversation',
251253

254+
ollamaEndpoint: 'http://127.0.0.1:11434',
255+
ollamaModelName: 'gemma2',
256+
keepAliveTime: '5m',
257+
252258
// advanced
253259

254260
maxResponseTokenLength: 1000,
@@ -281,6 +287,7 @@ export const defaultConfig = {
281287
'moonshotWebFree',
282288
'chatglmTurbo',
283289
'customModel',
290+
'ollamaModel',
284291
'azureOpenAi',
285292
],
286293
activeSelectionTools: ['translate', 'summary', 'polish', 'code', 'ask'],
@@ -381,6 +388,10 @@ export function isUsingCustomModel(configOrSession) {
381388
return customApiModelKeys.includes(configOrSession.modelName)
382389
}
383390

391+
export function isUsingOllamaModel(configOrSession) {
392+
return ollamaApiModelKeys.includes(configOrSession.modelName)
393+
}
394+
384395
export function isUsingChatGLMApi(configOrSession) {
385396
return chatglmApiModelKeys.includes(configOrSession.modelName)
386397
}

src/content-script/index.jsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ async function mountComponent(siteConfig, userConfig) {
7272
})
7373

7474
const position = {
75-
x: window.innerWidth - 300 - (Math.floor((20 / 100) * window.innerWidth)),
75+
x: window.innerWidth - 300 - Math.floor((20 / 100) * window.innerWidth),
7676
y: window.innerHeight / 2 - 200,
7777
}
7878
const toolbarContainer = createElementAtPosition(position.x, position.y)

src/manifest.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "ChatGPTBox",
33
"description": "Integrating ChatGPT into your browser deeply, everything you need is here",
4-
"version": "2.5.6",
4+
"version": "2.5.7",
55
"manifest_version": 3,
66
"icons": {
77
"16": "logo.png",

src/manifest.v2.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"name": "ChatGPTBox",
33
"description": "Integrating ChatGPT into your browser deeply, everything you need is here",
4-
"version": "2.5.6",
4+
"version": "2.5.7",
55
"manifest_version": 2,
66
"icons": {
77
"16": "logo.png",

src/popup/sections/AdvancedPart.jsx

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1+
import '../styles.scss'
12
import { useTranslation } from 'react-i18next'
23
import { parseFloatWithClamp, parseIntWithClamp } from '../../utils/index.mjs'
4+
import { isUsingOllamaModel } from '../../config/index.mjs'
35
import PropTypes from 'prop-types'
46
import { Tab, TabList, TabPanel, Tabs } from 'react-tabs'
57
import Browser from 'webextension-polyfill'
@@ -56,6 +58,49 @@ function ApiParams({ config, updateConfig }) {
5658
}}
5759
/>
5860
</label>
61+
{isUsingOllamaModel(config) && (
62+
<label>
63+
{`${t('keep-alive Time')}: `}
64+
<div className="label-group">
65+
<label>
66+
<input
67+
type="radio"
68+
name="keepAliveTime"
69+
value="5m"
70+
checked={config.keepAliveTime === '5m'}
71+
onChange={(e) => {
72+
updateConfig({ keepAliveTime: e.target.value })
73+
}}
74+
/>
75+
{t('5m')}
76+
</label>
77+
<label>
78+
<input
79+
type="radio"
80+
name="keepAliveTime"
81+
value="30m"
82+
checked={config.keepAliveTime === '30m'}
83+
onChange={(e) => {
84+
updateConfig({ keepAliveTime: e.target.value })
85+
}}
86+
/>
87+
{t('30m')}
88+
</label>
89+
<label>
90+
<input
91+
type="radio"
92+
name="keepAliveTime"
93+
value="-1"
94+
checked={config.keepAliveTime === '-1'}
95+
onChange={(e) => {
96+
updateConfig({ keepAliveTime: e.target.value })
97+
}}
98+
/>
99+
{t('Forever')}
100+
</label>
101+
</div>
102+
</label>
103+
)}
59104
</>
60105
)
61106
}

src/popup/sections/GeneralPart.jsx

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
isUsingClaudeApi,
99
isUsingCustomModel,
1010
isUsingCustomNameOnlyModel,
11+
isUsingOllamaModel,
1112
isUsingGithubThirdPartyApi,
1213
isUsingMultiModeModel,
1314
ModelMode,
@@ -163,6 +164,7 @@ export function GeneralPart({ config, updateConfig }) {
163164
isUsingOpenAiApiKey(config) ||
164165
isUsingMultiModeModel(config) ||
165166
isUsingCustomModel(config) ||
167+
isUsingOllamaModel(config) ||
166168
isUsingAzureOpenAi(config) ||
167169
isUsingClaudeApi(config) ||
168170
isUsingCustomNameOnlyModel(config) ||
@@ -271,6 +273,18 @@ export function GeneralPart({ config, updateConfig }) {
271273
}}
272274
/>
273275
)}
276+
{isUsingOllamaModel(config) && (
277+
<input
278+
style="width: 50%;"
279+
type="text"
280+
value={config.ollamaModelName}
281+
placeholder={t('Model Name')}
282+
onChange={(e) => {
283+
const ollamaModelName = e.target.value
284+
updateConfig({ ollamaModelName: ollamaModelName })
285+
}}
286+
/>
287+
)}
274288
{isUsingAzureOpenAi(config) && (
275289
<input
276290
type="password"
@@ -354,6 +368,28 @@ export function GeneralPart({ config, updateConfig }) {
354368
}}
355369
/>
356370
)}
371+
{isUsingOllamaModel(config) && (
372+
<input
373+
type="text"
374+
value={config.ollamaEndpoint}
375+
placeholder={t('Ollama Endpoint')}
376+
onChange={(e) => {
377+
const value = e.target.value
378+
updateConfig({ ollamaEndpoint: value })
379+
}}
380+
/>
381+
)}
382+
{isUsingOllamaModel(config) && (
383+
<input
384+
type="password"
385+
value={config.ollamaApiKey}
386+
placeholder={t('API Key')}
387+
onChange={(e) => {
388+
const apiKey = e.target.value
389+
updateConfig({ ollamaApiKey: apiKey })
390+
}}
391+
/>
392+
)}
357393
{isUsingAzureOpenAi(config) && (
358394
<input
359395
type="password"

src/popup/styles.scss

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,11 @@
2323
--active-color: #eaecf0;
2424
}
2525

26+
#app {
27+
max-width: 600px;
28+
margin: 0 auto;
29+
}
30+
2631
.container-page-mode {
2732
display: flex;
2833
flex-direction: column;
@@ -58,6 +63,7 @@
5863
}
5964

6065
.footer {
66+
max-width: 580px;
6167
width: 90%;
6268
position: fixed;
6369
bottom: 10px;
@@ -88,3 +94,12 @@
8894
background: var(--active-color);
8995
}
9096
}
97+
98+
.label-group {
99+
display: flex;
100+
align-items: center;
101+
}
102+
103+
.label-group label {
104+
margin-right: 10px;
105+
}

src/services/apis/ollama-api.mjs

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
// ollama api version
2+
3+
// There is a lot of duplicated code here, but it is very easy to refactor.
4+
// The current state is mainly convenient for making targeted changes at any time,
5+
// and it has not yet had a negative impact on maintenance.
6+
// If necessary, I will refactor.
7+
8+
import { getUserConfig } from '../../config/index.mjs'
9+
import { fetchSSE } from '../../utils/fetch-ollama.mjs'
10+
import { getConversationPairs } from '../../utils/get-conversation-pairs.mjs'
11+
import { isEmpty } from 'lodash-es'
12+
import { pushRecord, setAbortController } from './shared.mjs'
13+
14+
/**
15+
* @param {Browser.Runtime.Port} port
16+
* @param {string} question
17+
* @param {Session} session
18+
* @param {string} apiKey
19+
* @param {string} modelName
20+
*/
21+
export async function generateAnswersWithOllamaApi(port, question, session, apiKey, modelName) {
22+
const { controller, messageListener, disconnectListener } = setAbortController(port)
23+
24+
const config = await getUserConfig()
25+
const prompt = getConversationPairs(
26+
session.conversationRecords.slice(-config.maxConversationContextLength),
27+
false,
28+
)
29+
// prompt.unshift({ role: 'system', content: await getOllamaApiPromptBase() })
30+
prompt.push({ role: 'user', content: question })
31+
const apiUrl = config.ollamaEndpoint
32+
33+
let answer = ''
34+
let finished = false
35+
const finish = () => {
36+
finished = true
37+
pushRecord(session, question, answer)
38+
console.debug('conversation history', { content: session.conversationRecords })
39+
port.postMessage({ answer: null, done: true, session: session })
40+
}
41+
await fetchSSE(`${apiUrl}/api/chat`, {
42+
method: 'POST',
43+
signal: controller.signal,
44+
headers: {
45+
'Content-Type': 'application/json',
46+
Authorization: `Bearer ${apiKey}`,
47+
},
48+
body: JSON.stringify({
49+
messages: prompt,
50+
model: modelName,
51+
stream: true,
52+
keep_alive: config.keepAliveTime === '-1' ? -1 : config.keepAliveTime,
53+
}),
54+
onMessage(message) {
55+
console.debug('sse message', message)
56+
if (finished) return
57+
let data = message
58+
const delta = data.message?.content
59+
if (delta) {
60+
answer += delta
61+
port.postMessage({ answer: answer, done: false, session: null })
62+
}
63+
if (data.done_reason) {
64+
finish()
65+
return
66+
}
67+
},
68+
async onStart() {},
69+
async onEnd() {
70+
port.postMessage({ done: true })
71+
port.onMessage.removeListener(messageListener)
72+
port.onDisconnect.removeListener(disconnectListener)
73+
},
74+
async onError(resp) {
75+
port.onMessage.removeListener(messageListener)
76+
port.onDisconnect.removeListener(disconnectListener)
77+
if (resp instanceof Error) throw resp
78+
const error = await resp.json().catch(() => ({}))
79+
throw new Error(!isEmpty(error) ? JSON.stringify(error) : `${resp.status} ${resp.statusText}`)
80+
},
81+
})
82+
}

0 commit comments

Comments
 (0)