feat: support DeepSeek, OpenRouter & Ollama providers
This commit is contained in:
10
README.md
10
README.md
@ -15,7 +15,7 @@ Features:
|
||||
|
||||
Currently available providers:
|
||||
|
||||
- AI: OpenAI compatible
|
||||
- AI: OpenAI compatible, DeepSeek, OpenRouter, Ollama
|
||||
- Web Search: Tavily (similar to Firecrawl, but with more free quota (1000 credits / month))
|
||||
|
||||
Please give a 🌟 Star if you like this project!
|
||||
@ -24,6 +24,13 @@ Please give a 🌟 Star if you like this project!
|
||||
|
||||
## Recent updates
|
||||
|
||||
25/02/15
|
||||
|
||||
- Added provider support for DeepSeek, OpenRouter and Ollama
|
||||
- Supported checking project updates
|
||||
- Supported regenerating reports
|
||||
- General fixes
|
||||
|
||||
25/02/14
|
||||
|
||||
- Supported reasoning models like DeepSeek R1
|
||||
@ -37,6 +44,7 @@ Please give a 🌟 Star if you like this project!
|
||||
- Fixed "export as PDF" issues
|
||||
|
||||
25/02/12
|
||||
|
||||
- Added Chinese translation. The models will respond in the user's language.
|
||||
- Various fixes
|
||||
|
||||
|
15
README_zh.md
15
README_zh.md
@ -3,6 +3,7 @@
|
||||
本项目是 https://github.com/dzhng/deep-research 的可视化版本,并做了一些改进。
|
||||
|
||||
特色:
|
||||
|
||||
- 🚀 **隐私安全**:所有配置和 API 请求均在浏览器端完成
|
||||
- 🕙 **实时反馈**:流式传输 AI 响应并在界面实时展示
|
||||
- 🌳 **搜索可视化**:使用树状结构展示研究过程,支持使用英文搜索词
|
||||
@ -11,14 +12,19 @@
|
||||
|
||||
当前支持的供应商:
|
||||
|
||||
- AI 服务:任意兼容 OpenAPI 的供应商
|
||||
- AI 服务:OpenAPI 兼容、DeepSeek、OpenRouter、Ollama
|
||||
- 网络搜索:Tavily(类似 Firecrawl,提供每月 1000 次免费搜索)
|
||||
|
||||
喜欢本项目请点 ⭐ 收藏!
|
||||
喜欢本项目请点 ⭐ 收藏! <video width="500" src="https://github.com/user-attachments/assets/2f5a6f9c-18d1-4d40-9822-2de260d55dab" controls></video>
|
||||
|
||||
<video width="500" src="https://github.com/user-attachments/assets/2f5a6f9c-18d1-4d40-9822-2de260d55dab" controls></video>
|
||||
## 近期更新
|
||||
|
||||
## 最近更新
|
||||
25/02/15
|
||||
|
||||
- AI 提供商支持 DeepSeek,OpenRouter 和 Ollama
|
||||
- 支持检查项目更新
|
||||
- 支持重新生成报告
|
||||
- 一般性优化和改进
|
||||
|
||||
25/02/14
|
||||
|
||||
@ -33,6 +39,7 @@
|
||||
- 修复“导出 PDF”不可用的问题
|
||||
|
||||
25/02/12
|
||||
|
||||
- 添加中文支持。模型会自动使用用户的语言回答了。
|
||||
- 修复一些 bug
|
||||
|
||||
|
@ -24,11 +24,20 @@
|
||||
{
|
||||
label: t('settings.ai.providers.openaiCompatible.title'),
|
||||
help: t('settings.ai.providers.openaiCompatible.description'),
|
||||
apiBasePlaceholder: t(
|
||||
'settings.ai.providers.openaiCompatible.apiBasePlaceholder',
|
||||
),
|
||||
value: 'openai-compatible',
|
||||
},
|
||||
{
|
||||
label: 'DeepSeek',
|
||||
value: 'deepseek',
|
||||
},
|
||||
{
|
||||
label: 'OpenRouter',
|
||||
value: 'openrouter',
|
||||
},
|
||||
{
|
||||
label: 'Ollama',
|
||||
value: 'ollama',
|
||||
},
|
||||
])
|
||||
const selectedAiProvider = computed(() =>
|
||||
aiProviderOptions.value.find((o) => o.value === config.value.ai.provider),
|
||||
@ -110,15 +119,22 @@
|
||||
<h3 class="font-bold">{{ $t('settings.ai.provider') }}</h3>
|
||||
<UFormField>
|
||||
<template v-if="selectedAiProvider" #help>
|
||||
{{ selectedAiProvider.help }}
|
||||
<span class="whitespace-pre-wrap">
|
||||
{{ selectedAiProvider.help }}
|
||||
</span>
|
||||
</template>
|
||||
<USelect v-model="config.ai.provider" :items="aiProviderOptions" />
|
||||
<USelect
|
||||
v-model="config.ai.provider"
|
||||
class="w-50"
|
||||
:items="aiProviderOptions"
|
||||
/>
|
||||
</UFormField>
|
||||
<div
|
||||
v-if="config.ai.provider === 'openai-compatible'"
|
||||
class="flex flex-col gap-y-2"
|
||||
>
|
||||
<UFormField :label="$t('settings.ai.apiKey')" required>
|
||||
|
||||
<div class="flex flex-col gap-y-2">
|
||||
<UFormField
|
||||
:label="$t('settings.ai.apiKey')"
|
||||
:required="config.ai.provider !== 'ollama'"
|
||||
>
|
||||
<PasswordInput
|
||||
v-model="config.ai.apiKey"
|
||||
class="w-full"
|
||||
@ -129,7 +145,7 @@
|
||||
<UInput
|
||||
v-model="config.ai.apiBase"
|
||||
class="w-full"
|
||||
:placeholder="selectedAiProvider?.apiBasePlaceholder"
|
||||
:placeholder="aiApiBase"
|
||||
/>
|
||||
</UFormField>
|
||||
<UFormField :label="$t('settings.ai.model')" required>
|
||||
|
@ -94,7 +94,7 @@
|
||||
}
|
||||
} catch (e: any) {
|
||||
console.error('Error getting feedback:', e)
|
||||
if (e.message.includes('Failed to fetch')) {
|
||||
if (e.message?.includes('Failed to fetch')) {
|
||||
e.message += `\n${t('error.requestBlockedByCORS')}`
|
||||
}
|
||||
error.value = t('modelFeedback.error', [e.message])
|
||||
|
@ -1,19 +1,45 @@
|
||||
import { createDeepSeek } from '@ai-sdk/deepseek'
|
||||
import { extractReasoningMiddleware, wrapLanguageModel } from 'ai'
|
||||
import { createOpenRouter } from '@openrouter/ai-sdk-provider'
|
||||
import { createOpenAI } from '@ai-sdk/openai'
|
||||
import {
|
||||
extractReasoningMiddleware,
|
||||
wrapLanguageModel,
|
||||
type LanguageModelV1,
|
||||
} from 'ai'
|
||||
|
||||
export const useAiModel = () => {
|
||||
const config = useConfigStore()
|
||||
let model: LanguageModelV1
|
||||
|
||||
switch (config.config.ai.provider) {
|
||||
case 'openai-compatible':
|
||||
const deepseek = createDeepSeek({
|
||||
case 'openrouter': {
|
||||
const openRouter = createOpenRouter({
|
||||
apiKey: config.config.ai.apiKey,
|
||||
baseURL: config.aiApiBase,
|
||||
})
|
||||
return wrapLanguageModel({
|
||||
model: deepseek(config.config.ai.model),
|
||||
middleware: extractReasoningMiddleware({ tagName: 'think' }),
|
||||
model = openRouter(config.config.ai.model, {
|
||||
includeReasoning: true,
|
||||
})
|
||||
default:
|
||||
throw new Error(`Unknown AI provider: ${config.config.ai.provider}`)
|
||||
}
|
||||
case 'deepseek': {
|
||||
const deepSeek = createDeepSeek({
|
||||
apiKey: config.config.ai.apiKey,
|
||||
baseURL: config.aiApiBase,
|
||||
})
|
||||
model = deepSeek(config.config.ai.model)
|
||||
}
|
||||
case 'openai-compatible':
|
||||
default: {
|
||||
const openai = createOpenAI({
|
||||
apiKey: config.config.ai.apiKey,
|
||||
baseURL: config.aiApiBase,
|
||||
})
|
||||
model = openai(config.config.ai.model)
|
||||
}
|
||||
}
|
||||
|
||||
return wrapLanguageModel({
|
||||
model,
|
||||
middleware: extractReasoningMiddleware({ tagName: 'think' }),
|
||||
})
|
||||
}
|
||||
|
@ -22,8 +22,7 @@
|
||||
"providers": {
|
||||
"openaiCompatible": {
|
||||
"title": "OpenAI Compatible",
|
||||
"description": "Currently only supports OpenAI compatible providers, e.g. Gemini, Together AI, DeepSeek, SiliconCloud, ...",
|
||||
"apiBasePlaceholder": "https://api.openai.com/v1"
|
||||
"description": "e.g. OpenAI, Gemini, Together AI, SiliconCloud, ...\n(Note: DeepSeek, OpenRouter and Ollama now have their own providers.)"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -21,9 +21,8 @@
|
||||
"contextSizeHelp": "上下文的最大大小(以 token 计)。这是将发送给模型的最大 token 数量。默认值为 128,000 个 token。",
|
||||
"providers": {
|
||||
"openaiCompatible": {
|
||||
"title": "OpenAI Compatiible",
|
||||
"description": "目前仅支持与 OpenAI 兼容的提供商,如 Gemini、Together AI、DeepSeek、SiliconCloud……",
|
||||
"apiBasePlaceholder": "https://api.openai.com/v1"
|
||||
"title": "OpenAI Compatible",
|
||||
"description": "如 OpenAI、Gemini、Together AI、SiliconCloud……\n注:DeepSeek、OpenRouter 和 Ollama 现在已经有了独立选项,请切换使用。"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -38,7 +38,7 @@ export function generateFeedback({
|
||||
prompt,
|
||||
onError({ error }) {
|
||||
console.error(`generateFeedback`, error)
|
||||
throw error
|
||||
throw error instanceof Error ? error : new Error(String(error))
|
||||
},
|
||||
})
|
||||
|
||||
|
@ -20,6 +20,7 @@
|
||||
"@nuxt/ui": "3.0.0-alpha.12",
|
||||
"@nuxtjs/color-mode": "^3.5.2",
|
||||
"@nuxtjs/i18n": "9.2.0",
|
||||
"@openrouter/ai-sdk-provider": "^0.2.1",
|
||||
"@pinia/nuxt": "^0.10.1",
|
||||
"@tailwindcss/typography": "^0.5.16",
|
||||
"@tavily/core": "^0.3.1",
|
||||
|
41
pnpm-lock.yaml
generated
41
pnpm-lock.yaml
generated
@ -32,6 +32,9 @@ importers:
|
||||
'@nuxtjs/i18n':
|
||||
specifier: 9.2.0
|
||||
version: 9.2.0(@vue/compiler-dom@3.5.13)(eslint@9.20.1(jiti@2.4.2))(magicast@0.3.5)(rollup@4.34.6)(typescript@5.7.3)(vue@3.5.13(typescript@5.7.3))
|
||||
'@openrouter/ai-sdk-provider':
|
||||
specifier: ^0.2.1
|
||||
version: 0.2.1(zod@3.24.2)
|
||||
'@pinia/nuxt':
|
||||
specifier: ^0.10.1
|
||||
version: 0.10.1(magicast@0.3.5)(pinia@3.0.1(typescript@5.7.3)(vue@3.5.13(typescript@5.7.3)))
|
||||
@ -108,6 +111,15 @@ packages:
|
||||
peerDependencies:
|
||||
zod: ^3.0.0
|
||||
|
||||
'@ai-sdk/provider-utils@2.1.5':
|
||||
resolution: {integrity: sha512-PcNR7E4ovZGV/J47gUqaFlvzorgca6uUfN5WzfXJSFWeOeLunN+oxRVwgUOwj0zbmO0yGQTHQD+FHVw8s3Rz8w==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
zod: ^3.0.0
|
||||
peerDependenciesMeta:
|
||||
zod:
|
||||
optional: true
|
||||
|
||||
'@ai-sdk/provider-utils@2.1.8':
|
||||
resolution: {integrity: sha512-1j9niMUAFlCBdYRYJr1yoB5kwZcRFBVuBiL1hhrf0ONFNrDiJYA6F+gROOuP16NHhezMfTo60+GeeV1xprHFjg==}
|
||||
engines: {node: '>=18'}
|
||||
@ -117,6 +129,10 @@ packages:
|
||||
zod:
|
||||
optional: true
|
||||
|
||||
'@ai-sdk/provider@1.0.6':
|
||||
resolution: {integrity: sha512-hwj/gFNxpDgEfTaYzCYoslmw01IY9kWLKl/wf8xuPvHtQIzlfXWmmUwc8PnCwxyt8cKzIuV0dfUghCf68HQ0SA==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
'@ai-sdk/provider@1.0.7':
|
||||
resolution: {integrity: sha512-q1PJEZ0qD9rVR+8JFEd01/QM++csMT5UVwYXSN2u54BrVw/D8TZLTeg2FEfKK00DgAx0UtWd8XOhhwITP9BT5g==}
|
||||
engines: {node: '>=18'}
|
||||
@ -777,6 +793,12 @@ packages:
|
||||
resolution: {integrity: sha512-R7azgNji8jIZdimlylK1CU4plO1OjRPZduTyjS9SHHasMCzxrPM+LBJLRzjt9NUNatquLeCcVfHAYvxIxPHCmg==}
|
||||
engines: {node: ^14.16.0 || >=16.11.0}
|
||||
|
||||
'@openrouter/ai-sdk-provider@0.2.1':
|
||||
resolution: {integrity: sha512-Iz+wpGR6001OfbYPp+VmXFZBNpF6a3uN5gzgEBkNCqwZUzuYANO03d4eSgqFrDvfsenG7eE9hpKHB4zIg8YmKA==}
|
||||
engines: {node: '>=18'}
|
||||
peerDependencies:
|
||||
zod: ^3.0.0
|
||||
|
||||
'@opentelemetry/api@1.9.0':
|
||||
resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==}
|
||||
engines: {node: '>=8.0.0'}
|
||||
@ -4293,6 +4315,15 @@ snapshots:
|
||||
'@ai-sdk/provider-utils': 2.1.8(zod@3.24.2)
|
||||
zod: 3.24.2
|
||||
|
||||
'@ai-sdk/provider-utils@2.1.5(zod@3.24.2)':
|
||||
dependencies:
|
||||
'@ai-sdk/provider': 1.0.6
|
||||
eventsource-parser: 3.0.0
|
||||
nanoid: 3.3.8
|
||||
secure-json-parse: 2.7.0
|
||||
optionalDependencies:
|
||||
zod: 3.24.2
|
||||
|
||||
'@ai-sdk/provider-utils@2.1.8(zod@3.24.2)':
|
||||
dependencies:
|
||||
'@ai-sdk/provider': 1.0.7
|
||||
@ -4302,6 +4333,10 @@ snapshots:
|
||||
optionalDependencies:
|
||||
zod: 3.24.2
|
||||
|
||||
'@ai-sdk/provider@1.0.6':
|
||||
dependencies:
|
||||
json-schema: 0.4.0
|
||||
|
||||
'@ai-sdk/provider@1.0.7':
|
||||
dependencies:
|
||||
json-schema: 0.4.0
|
||||
@ -5329,6 +5364,12 @@ snapshots:
|
||||
- typescript
|
||||
- vue
|
||||
|
||||
'@openrouter/ai-sdk-provider@0.2.1(zod@3.24.2)':
|
||||
dependencies:
|
||||
'@ai-sdk/provider': 1.0.6
|
||||
'@ai-sdk/provider-utils': 2.1.5(zod@3.24.2)
|
||||
zod: 3.24.2
|
||||
|
||||
'@opentelemetry/api@1.9.0': {}
|
||||
|
||||
'@parcel/watcher-android-arm64@2.5.1':
|
||||
|
@ -1,3 +1,3 @@
|
||||
{
|
||||
"version": "1.0.4"
|
||||
"version": "1.0.5"
|
||||
}
|
@ -1,7 +1,12 @@
|
||||
import { skipHydrate } from 'pinia'
|
||||
import type { Locale } from '~/components/LangSwitcher.vue'
|
||||
|
||||
export type ConfigAiProvider = 'openai-compatible'
|
||||
export type ConfigAiProvider =
|
||||
| 'openai-compatible'
|
||||
| 'openrouter'
|
||||
| 'deepseek'
|
||||
| 'ollama'
|
||||
|
||||
export interface ConfigAi {
|
||||
provider: ConfigAiProvider
|
||||
apiKey?: string
|
||||
@ -39,6 +44,15 @@ export const useConfigStore = defineStore('config', () => {
|
||||
)
|
||||
|
||||
const aiApiBase = computed(() => {
|
||||
if (config.value.ai.provider === 'openrouter') {
|
||||
return config.value.ai.apiBase || 'https://openrouter.ai/api/v1'
|
||||
}
|
||||
if (config.value.ai.provider === 'deepseek') {
|
||||
return config.value.ai.apiBase || 'https://api.deepseek.com/v1'
|
||||
}
|
||||
if (config.value.ai.provider === 'ollama') {
|
||||
return config.value.ai.apiBase || 'http://localhost:11434/v1'
|
||||
}
|
||||
return config.value.ai.apiBase || 'https://api.openai.com/v1'
|
||||
})
|
||||
|
||||
|
Reference in New Issue
Block a user