feat: support reasoning models like DeepSeek R1

This commit is contained in:
AnotiaWang
2025-02-14 15:20:02 +08:00
parent 93527597b7
commit e7296df78f
17 changed files with 549 additions and 171 deletions

30
.dockerignore Normal file
View File

@ -0,0 +1,30 @@
# Nuxt dev/build outputs
.output
.data
.nuxt
.nitro
.cache
dist
# Node dependencies
node_modules
# Logs
logs
*.log
# Misc
.DS_Store
.fleet
.idea
# Local env files
.env
.env.*
!.env.example
# TypeScript
*.tsbuildinfo
# Nuxt generate
.generate

View File

@ -8,10 +8,10 @@ Features:
- 🚀 **Safe & Secure**: Everything (config, API requests, ...) stays in your browser locally
- 🕙 **Realtime feedback**: Stream AI responses and reflect on the UI in real-time
- 🌳 **Search visualization**: Shows the research process using a tree structure
- 🌳 **Search visualization**: Shows the research process using a tree structure. Supports searching in different languages
- 📄 **Export as PDF**: Export the final research report as a PDF
- 🌐 **Search in different languages**: Useful when you want to get search results in a different language
- 🤖 **Supports more models**: Uses plain prompts instead of newer, less widely supported features like Structured Outputs. This ensures to work with more providers that haven't caught up with the latest OpenAI capabilities.
- 🐳 **Docker support**: Deploy in your environment in one-line command
Currently available providers:
@ -20,7 +20,25 @@ Currently available providers:
Please give a 🌟 Star if you like this project!
<video 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>
## Recent updates
25/02/14
- Supported reasoning models like DeepSeek R1
- Improved compatibility with more models & error handling
25/02/13
- Significantly reduced bundle size
- Supported searching in different languages
- Added Docker support
- Fixed "export as PDF" issues
25/02/12
- Added Chinese translation. The models will respond in the user's language.
- Various fixes
## How to use

View File

@ -16,7 +16,25 @@
喜欢本项目请点 ⭐ 收藏!
<video 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/14
- 支持 DeepSeek R1 等思维链模型
- 改进了模型兼容性,改进异常处理
25/02/13
- 大幅缩减了网页体积
- 支持配置搜索时使用的语言
- 支持 Docker 部署
- 修复“导出 PDF”不可用的问题
25/02/12
- 添加中文支持。模型会自动使用用户的语言回答了。
- 修复一些 bug
## 使用指南

View File

@ -38,6 +38,14 @@
}
switch (step.type) {
case 'generating_query_reasoning': {
if (node) {
node.generateQueriesReasoning =
(node.generateQueriesReasoning ?? '') + step.delta
}
break
}
case 'generating_query': {
if (!node) {
// 创建新节点
@ -86,10 +94,17 @@
break
}
case 'processing_serach_result_reasoning': {
if (node) {
node.generateLearningsReasoning =
(node.generateLearningsReasoning ?? '') + step.delta
}
break
}
case 'processing_serach_result': {
if (node) {
node.learnings = step.result.learnings || []
node.followUpQuestions = step.result.followUpQuestions || []
}
break
}
@ -157,6 +172,8 @@
selectedNode.value = undefined
searchResults.value = {}
isLoading.value = true
// Clear the root node's reasoning content
tree.value.generateQueriesReasoning = ''
try {
const searchLanguage = config.value.webSearch.searchLanguage
? t('language', {}, { locale: config.value.webSearch.searchLanguage })
@ -211,20 +228,28 @@
{{ selectedNode.label ?? $t('webBrowsing.generating') }}
</h2>
<!-- Set loading default to true, because currently don't know how to handle it otherwise -->
<ReasoningAccordion
v-model="selectedNode.generateQueriesReasoning"
loading
/>
<!-- Research goal -->
<h3 class="text-lg font-semibold mt-2">
{{ t('webBrowsing.researchGoal') }}
</h3>
<!-- Root node has no additional information -->
<p v-if="selectedNode.id === '0'">
{{ t('webBrowsing.startNode.description') }}
</p>
<template v-else>
<h3 class="text-lg font-semibold mt-2">
{{ t('webBrowsing.researchGoal') }}
</h3>
<p
v-if="selectedNode.researchGoal"
class="prose max-w-none"
v-html="marked(selectedNode.researchGoal, { gfm: true })"
/>
<!-- Visited URLs -->
<h3 class="text-lg font-semibold mt-2">
{{ t('webBrowsing.visitedUrls') }}
</h3>
@ -245,9 +270,15 @@
</li>
</ul>
<!-- Learnings -->
<h3 class="text-lg font-semibold mt-2">
{{ t('webBrowsing.learnings') }}
</h3>
<ReasoningAccordion
v-model="selectedNode.generateQueriesReasoning"
loading
/>
<p
v-for="(learning, index) in selectedNode.learnings"
class="prose max-w-none"

View File

@ -0,0 +1,52 @@
<!-- Shows an accordion for reasoning (CoT) content. The accordion is default invisible,
until modelValue's length > 0 -->
<script setup lang="ts">
const props = defineProps<{
loading?: boolean
}>()
const modelValue = defineModel<string>()
const items = computed(() => [
{
icon: 'i-lucide-brain',
content: modelValue.value,
},
])
const currentOpen = ref('0')
watchEffect(() => {
if (props.loading) {
currentOpen.value = '0'
} else {
currentOpen.value = '-1'
}
})
</script>
<template>
<UAccordion
v-if="modelValue"
v-model="currentOpen"
class="border border-gray-200 dark:border-gray-800 rounded-lg px-3 sm:px-4"
:items="items"
:loading="loading"
>
<template #leading="{ item }">
<div
:class="[
loading && 'animate-pulse',
'flex items-center gap-2 text-(--ui-primary)',
]"
>
<UIcon :name="item.icon" size="20" />
{{ loading ? $t('modelThinking') : $t('modelThinkingComplete') }}
</div>
</template>
<template #content="{ item }">
<p class="text-sm text-gray-500 whitespace-pre-wrap mb-4">
{{ item.content }}
</p>
</template>
</UAccordion>
</template>

View File

@ -15,6 +15,7 @@
}>()
const { t, locale } = useI18n()
const reasoningContent = ref('')
const feedback = ref<ResearchFeedbackResult[]>([])
const isLoading = ref(false)
@ -39,17 +40,27 @@
numQuestions,
language: t('language', {}, { locale: locale.value }),
})) {
const questions = f.questions!.filter((s) => typeof s === 'string')
// Incrementally update modelValue
for (let i = 0; i < questions.length; i += 1) {
if (feedback.value[i]) {
feedback.value[i].assistantQuestion = questions[i]
} else {
feedback.value.push({
assistantQuestion: questions[i],
userAnswer: '',
})
if (f.type === 'reasoning') {
reasoningContent.value += f.delta
} else if (f.type === 'error') {
error.value = f.message
} else if (f.type === 'object') {
const questions = f.value.questions!.filter(
(s) => typeof s === 'string',
)
// Incrementally update modelValue
for (let i = 0; i < questions.length; i += 1) {
if (feedback.value[i]) {
feedback.value[i].assistantQuestion = questions[i]
} else {
feedback.value.push({
assistantQuestion: questions[i],
userAnswer: '',
})
}
}
} else if (f.type === 'bad-end') {
error.value = t('invalidStructuredOutput')
}
}
} catch (e: any) {
@ -66,6 +77,7 @@
function clear() {
feedback.value = []
error.value = ''
reasoningContent.value = ''
}
defineExpose({
@ -85,13 +97,16 @@
</template>
<div class="flex flex-col gap-2">
<div v-if="!feedback.length && !error">
<div v-if="!feedback.length && !reasoningContent && !error">
{{ $t('modelFeedback.waiting') }}
</div>
<template v-else>
<div v-if="error" class="text-red-500 whitespace-pre-wrap">
{{ error }}
</div>
<ReasoningAccordion v-model="reasoningContent" :loading="isLoading" />
<div
v-for="(feedback, index) in feedback"
class="flex flex-col gap-2"

View File

@ -102,7 +102,7 @@
block
@click="handleSubmit"
>
{{ isLoadingFeedback ? 'Researching...' : $t('researchTopic.start') }}
{{ isLoadingFeedback ? $t('researchTopic.researching') : $t('researchTopic.start') }}
</UButton>
</template>
</UCard>

View File

@ -16,6 +16,7 @@
const error = ref('')
const loading = ref(false)
const loadingExportPdf = ref(false)
const reasoningContent = ref('')
const reportContent = ref('')
const reportHtml = computed(() =>
marked(reportContent.value, { silent: true, gfm: true, breaks: true }),
@ -29,9 +30,20 @@
loading.value = true
error.value = ''
reportContent.value = ''
reasoningContent.value = ''
try {
for await (const chunk of writeFinalReport(params).textStream) {
reportContent.value += chunk
for await (const chunk of writeFinalReport(params).fullStream) {
if (chunk.type === 'reasoning') {
reasoningContent.value += chunk.textDelta
} else if (chunk.type === 'text-delta') {
reportContent.value += chunk.textDelta
} else if (chunk.type === 'error') {
error.value = t('researchReport.error', [
chunk.error instanceof Error
? chunk.error.message
: String(chunk.error),
])
}
}
reportContent.value += `\n\n## ${t(
'researchReport.sources',
@ -158,21 +170,25 @@
</div>
</template>
<div v-if="error" class="text-red-500">{{ error }}</div>
<ReasoningAccordion
v-if="reasoningContent"
v-model="reasoningContent"
class="mb-4"
:loading="loading"
/>
<div
v-if="reportContent"
id="report-content"
class="prose prose-sm max-w-none p-6 bg-gray-50 dark:bg-gray-800 dark:text-white rounded-lg shadow"
v-html="reportHtml"
/>
<template v-else>
<div v-if="error" class="text-red-500">{{ error }}</div>
<div v-else>
{{
loading
? $t('researchReport.generating')
: $t('researchReport.waiting')
}}
</div>
</template>
<div v-else>
{{
loading ? $t('researchReport.generating') : $t('researchReport.waiting')
}}
</div>
</UCard>
</template>

View File

@ -6,11 +6,15 @@
export type TreeNode = {
id: string
/** Label, represents the search query */
/** Label, represents the search query. Generated from parent node. */
label: string
/** The research goal of this node. Generated from parent node. */
researchGoal?: string
/** Reasoning content when generating queries for the next iteration. */
generateQueriesReasoning?: string
/** Reasoning content when generating learnings for this iteration. */
generateLearningsReasoning?: string
learnings?: string[]
followUpQuestions?: string[]
visitedUrls?: string[]
status?: TreeNodeStatus
children: TreeNode[]

View File

@ -1,14 +1,18 @@
import { createOpenAI } from '@ai-sdk/openai'
import { createDeepSeek } from '@ai-sdk/deepseek'
import { extractReasoningMiddleware, wrapLanguageModel } from 'ai'
export const useAiModel = () => {
const config = useConfigStore()
switch (config.config.ai.provider) {
case 'openai-compatible':
const openai = createOpenAI({
const deepseek = createDeepSeek({
apiKey: config.config.ai.apiKey,
baseURL: config.aiApiBase,
})
return openai(config.config.ai.model)
return wrapLanguageModel({
model: deepseek(config.config.ai.model),
middleware: extractReasoningMiddleware({ tagName: 'think' }),
})
default:
throw new Error(`Unknown AI provider: ${config.config.ai.provider}`)
}

View File

@ -1,5 +1,8 @@
{
"language": "English",
"modelThinking": "Thinking...",
"modelThinkingComplete": "Thinking complete",
"invalidStructuredOutput": "The model returned content which is incomplete or invalid and cannot be parsed. Try using a bigger or \"smarter\" model.",
"index": {
"projectDescription": "This is a web UI for {0} that allows AI to search online and dig deeper on its own based on specific questions, and then output a research report.\nThis project features streaming AI responses for realtime feedback, and visualization of the research process using a tree structure.\nAll API requests are directly sent from your browser. No remote data stored.",
"missingConfigTitle": "Config not set",

View File

@ -1,5 +1,8 @@
{
"language": "中文",
"modelThinking": "思考中...",
"modelThinkingComplete": "思考完毕",
"invalidStructuredOutput": "模型返回的内容无效或不完整,无法解析。请尝试换一个更大或者更“聪明”的模型。",
"index": {
"projectDescription": "Deep Research 是 {0} 的可视化 UI可以让 AI 根据特定问题联网搜索并自行深挖,并输出研究报告。\n本项目可以流式传输 AI 的回答来实时反馈,并使用树状结构可视化搜索过程。\n全部 API 请求都在浏览器本地完成。",
"missingConfigTitle": "需要配置 API",
@ -45,7 +48,7 @@
"breadth": "研究广度 (Breadth)",
"breadthHelp": "第一次迭代中的搜索次数。后续每轮迭代的搜索次数为上一轮的一半。",
"start": "开始研究",
"researching": "正在研究..."
"researching": "研究..."
},
"modelFeedback": {
"title": "2. 模型反馈",

View File

@ -29,6 +29,7 @@ export type PartialSearchResult = DeepPartial<SearchResult>
export type ResearchStep =
| { type: 'generating_query'; result: PartialSearchQuery; nodeId: string }
| { type: 'generating_query_reasoning'; delta: string; nodeId: string }
| {
type: 'generated_query'
query: string
@ -43,6 +44,11 @@ export type ResearchStep =
result: PartialSearchResult
nodeId: string
}
| {
type: 'processing_serach_result_reasoning'
delta: string
nodeId: string
}
| {
type: 'processed_search_result'
query: string
@ -110,7 +116,7 @@ export function generateSearchQueries({
'\n',
)}`
: '',
`You MUST respond in JSON with the following schema: ${jsonSchema}`,
`You MUST respond in JSON matching this JSON schema: ${jsonSchema}`,
lp,
].join('\n\n')
return streamText({
@ -160,7 +166,7 @@ function processSearchResult({
`<contents>${contents
.map((content) => `<content>\n${content}\n</content>`)
.join('\n')}</contents>`,
`You MUST respond in JSON with the following schema: ${jsonSchema}`,
`You MUST respond in JSON matching this JSON schema: ${jsonSchema}`,
languagePrompt(language),
].join('\n\n')
@ -234,6 +240,8 @@ export async function deepResearch({
/** Force the LLM to generate serp queries in a certain language */
searchLanguage?: string
}): Promise<ResearchResult> {
const { t } = useNuxtApp().$i18n
try {
const searchQueriesResult = generateSearchQueries({
query,
@ -246,12 +254,12 @@ export async function deepResearch({
let searchQueries: PartialSearchQuery[] = []
for await (const parsedQueries of parseStreamingJson(
searchQueriesResult.textStream,
for await (const chunk of parseStreamingJson(
searchQueriesResult.fullStream,
searchQueriesTypeSchema,
(value) => !!value.queries?.length && !!value.queries[0]?.query,
)) {
if (parsedQueries.queries) {
if (chunk.type === 'object' && chunk.value.queries) {
for (let i = 0; i < searchQueries.length; i++) {
onProgress({
type: 'generating_query',
@ -259,7 +267,27 @@ export async function deepResearch({
nodeId: childNodeId(nodeId, i),
})
}
searchQueries = parsedQueries.queries
searchQueries = chunk.value.queries
} else if (chunk.type === 'reasoning') {
onProgress({
type: 'generating_query_reasoning',
delta: chunk.delta,
nodeId,
})
} else if (chunk.type === 'error') {
onProgress({
type: 'error',
message: chunk.message,
nodeId,
})
break
} else if (chunk.type === 'bad-end') {
onProgress({
type: 'error',
message: t('invalidStructuredOutput'),
nodeId,
})
break
}
}
@ -272,6 +300,7 @@ export async function deepResearch({
})
}
// Run in parallel and limit the concurrency
const results = await Promise.all(
searchQueries.map((searchQuery, i) =>
limit(async () => {
@ -287,6 +316,7 @@ export async function deepResearch({
nodeId: childNodeId(nodeId, i),
})
try {
// Use Tavily to search the web
const result = await useTavily().search(searchQuery.query, {
maxResults: 5,
})
@ -314,18 +344,41 @@ export async function deepResearch({
})
let searchResult: PartialSearchResult = {}
for await (const parsedLearnings of parseStreamingJson(
searchResultGenerator.textStream,
for await (const chunk of parseStreamingJson(
searchResultGenerator.fullStream,
searchResultTypeSchema,
(value) => !!value.learnings?.length,
)) {
searchResult = parsedLearnings
onProgress({
type: 'processing_serach_result',
result: parsedLearnings,
query: searchQuery.query,
nodeId: childNodeId(nodeId, i),
})
const id = childNodeId(nodeId, i)
if (chunk.type === 'object') {
searchResult = chunk.value
onProgress({
type: 'processing_serach_result',
result: chunk.value,
query: searchQuery.query,
nodeId: id,
})
} else if (chunk.type === 'reasoning') {
onProgress({
type: 'processing_serach_result_reasoning',
delta: chunk.delta,
nodeId: id,
})
} else if (chunk.type === 'error') {
onProgress({
type: 'error',
message: chunk.message,
nodeId: id,
})
break
} else if (chunk.type === 'bad-end') {
onProgress({
type: 'error',
message: t('invalidStructuredOutput'),
nodeId: id,
})
break
}
}
console.log(
`Processed search result for ${searchQuery.query}`,

View File

@ -28,7 +28,7 @@ export function generateFeedback({
const jsonSchema = JSON.stringify(zodToJsonSchema(schema))
const prompt = [
`Given the following query from the user, ask ${numQuestions} follow up questions to clarify the research direction. Return a maximum of ${numQuestions} questions, but feel free to return less if the original query is clear: <query>${query}</query>`,
`You MUST respond in JSON with the following schema: ${jsonSchema}`,
`You MUST respond in JSON matching this JSON schema: ${jsonSchema}`,
languagePrompt(language),
].join('\n\n')
@ -37,12 +37,13 @@ export function generateFeedback({
system: systemPrompt(),
prompt,
onError({ error }) {
console.error(`generateFeedback`, error)
throw error
},
})
return parseStreamingJson(
stream.textStream,
stream.fullStream,
feedbackTypeSchema,
(value: PartialFeedback) => !!value.questions && value.questions.length > 0,
)

View File

@ -12,27 +12,28 @@
"postinstall": "nuxt prepare"
},
"dependencies": {
"@ai-sdk/openai": "^1.1.9",
"@ai-sdk/ui-utils": "^1.1.11",
"@ai-sdk/vue": "^1.1.11",
"@ai-sdk/deepseek": "^0.1.10",
"@ai-sdk/openai": "^1.1.11",
"@ai-sdk/ui-utils": "^1.1.14",
"@ai-sdk/vue": "^1.1.15",
"@iconify-json/lucide": "^1.2.26",
"@nuxt/ui": "3.0.0-alpha.12",
"@nuxtjs/color-mode": "^3.5.2",
"@nuxtjs/i18n": "9.2.0",
"@pinia/nuxt": "^0.10.0",
"@pinia/nuxt": "^0.10.1",
"@tailwindcss/typography": "^0.5.16",
"@tavily/core": "^0.0.3",
"ai": "^4.1.28",
"js-tiktoken": "^1.0.18",
"@tavily/core": "^0.3.1",
"ai": "^4.1.38",
"js-tiktoken": "^1.0.19",
"jspdf": "^2.5.2",
"marked": "^15.0.7",
"nuxt": "^3.15.4",
"p-limit": "^6.2.0",
"pinia": "^3.0.0",
"pinia": "^3.0.1",
"tailwindcss": "^4.0.5",
"vue": "latest",
"vue-router": "latest",
"zod": "^3.24.1",
"zod": "^3.24.2",
"zod-to-json-schema": "^3.24.1"
},
"devDependencies": {

277
pnpm-lock.yaml generated
View File

@ -8,15 +8,18 @@ importers:
.:
dependencies:
'@ai-sdk/deepseek':
specifier: ^0.1.10
version: 0.1.10(zod@3.24.2)
'@ai-sdk/openai':
specifier: ^1.1.9
version: 1.1.9(zod@3.24.1)
specifier: ^1.1.11
version: 1.1.11(zod@3.24.2)
'@ai-sdk/ui-utils':
specifier: ^1.1.11
version: 1.1.11(zod@3.24.1)
specifier: ^1.1.14
version: 1.1.14(zod@3.24.2)
'@ai-sdk/vue':
specifier: ^1.1.11
version: 1.1.11(vue@3.5.13(typescript@5.7.3))(zod@3.24.1)
specifier: ^1.1.15
version: 1.1.15(vue@3.5.13(typescript@5.7.3))(zod@3.24.2)
'@iconify-json/lucide':
specifier: ^1.2.26
version: 1.2.26
@ -30,20 +33,20 @@ importers:
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))
'@pinia/nuxt':
specifier: ^0.10.0
version: 0.10.0(magicast@0.3.5)(pinia@3.0.0(typescript@5.7.3)(vue@3.5.13(typescript@5.7.3)))
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)))
'@tailwindcss/typography':
specifier: ^0.5.16
version: 0.5.16(tailwindcss@4.0.6)
'@tavily/core':
specifier: ^0.0.3
version: 0.0.3
specifier: ^0.3.1
version: 0.3.1
ai:
specifier: ^4.1.28
version: 4.1.28(react@19.0.0)(zod@3.24.1)
specifier: ^4.1.38
version: 4.1.38(react@19.0.0)(zod@3.24.2)
js-tiktoken:
specifier: ^1.0.18
version: 1.0.18
specifier: ^1.0.19
version: 1.0.19
jspdf:
specifier: ^2.5.2
version: 2.5.2
@ -57,8 +60,8 @@ importers:
specifier: ^6.2.0
version: 6.2.0
pinia:
specifier: ^3.0.0
version: 3.0.0(typescript@5.7.3)(vue@3.5.13(typescript@5.7.3))
specifier: ^3.0.1
version: 3.0.1(typescript@5.7.3)(vue@3.5.13(typescript@5.7.3))
tailwindcss:
specifier: ^4.0.5
version: 4.0.6
@ -69,11 +72,11 @@ importers:
specifier: latest
version: 4.5.0(vue@3.5.13(typescript@5.7.3))
zod:
specifier: ^3.24.1
version: 3.24.1
specifier: ^3.24.2
version: 3.24.2
zod-to-json-schema:
specifier: ^3.24.1
version: 3.24.1(zod@3.24.1)
version: 3.24.1(zod@3.24.2)
devDependencies:
'@vueuse/core':
specifier: ^12.5.0
@ -84,14 +87,26 @@ importers:
packages:
'@ai-sdk/openai@1.1.9':
resolution: {integrity: sha512-t/CpC4TLipdbgBJTMX/otzzqzCMBSPQwUOkYPGbT/jyuC86F+YO9o+LS0Ty2pGUE1kyT+B3WmJ318B16ZCg4hw==}
'@ai-sdk/deepseek@0.1.10':
resolution: {integrity: sha512-JCYFEYog365yFXXrbySAwuYfe6zHKuBPmdOIa3xl0LSE0p1GcvGkZn80lNPLLzqR8GO8S4qUZoH+HL1kqYHaYA==}
engines: {node: '>=18'}
peerDependencies:
zod: ^3.0.0
'@ai-sdk/provider-utils@2.1.6':
resolution: {integrity: sha512-Pfyaj0QZS22qyVn5Iz7IXcJ8nKIKlu2MeSAdKJzTwkAks7zdLaKVB+396Rqcp1bfQnxl7vaduQVMQiXUrgK8Gw==}
'@ai-sdk/openai-compatible@0.1.10':
resolution: {integrity: sha512-AR5Acly7U64wDgVdVs9AqaMHgVps/35TMUMDS4akSZjsB4TdAhKnWdMXmACzCxmbCySHMoMnKY41XgvCyFN1pQ==}
engines: {node: '>=18'}
peerDependencies:
zod: ^3.0.0
'@ai-sdk/openai@1.1.11':
resolution: {integrity: sha512-gyqjoRvycmN3OGeK2SJXwhROv2ZZuP+SXbiAOoJf0ehWkqwkQSVaHigmg6OYLznmXusVHAvYD7SRgysXoJmuog==}
engines: {node: '>=18'}
peerDependencies:
zod: ^3.0.0
'@ai-sdk/provider-utils@2.1.8':
resolution: {integrity: sha512-1j9niMUAFlCBdYRYJr1yoB5kwZcRFBVuBiL1hhrf0ONFNrDiJYA6F+gROOuP16NHhezMfTo60+GeeV1xprHFjg==}
engines: {node: '>=18'}
peerDependencies:
zod: ^3.0.0
@ -103,8 +118,8 @@ packages:
resolution: {integrity: sha512-q1PJEZ0qD9rVR+8JFEd01/QM++csMT5UVwYXSN2u54BrVw/D8TZLTeg2FEfKK00DgAx0UtWd8XOhhwITP9BT5g==}
engines: {node: '>=18'}
'@ai-sdk/react@1.1.11':
resolution: {integrity: sha512-vfjZ7w2M+Me83HTMMrnnrmXotz39UDCMd27YQSrvt2f1YCLPloVpLhP+Y9TLZeFE/QiiRCrPYLDQm6aQJYJ9PQ==}
'@ai-sdk/react@1.1.14':
resolution: {integrity: sha512-4Y2d37l52TzOZNgH2KxXiWkJc4R7Yr+1k0VryOpZslqFHK0cclinFgWlclzF6Qn8C1pNMdhhxSEx1/N7SQZeKQ==}
engines: {node: '>=18'}
peerDependencies:
react: ^18 || ^19 || ^19.0.0-rc
@ -115,8 +130,8 @@ packages:
zod:
optional: true
'@ai-sdk/ui-utils@1.1.11':
resolution: {integrity: sha512-1SC9W4VZLcJtxHRv4Y0aX20EFeaEP6gUvVqoKLBBtMLOgtcZrv/F/HQRjGavGugiwlS3dsVza4X+E78fiwtlTA==}
'@ai-sdk/ui-utils@1.1.14':
resolution: {integrity: sha512-JQXcnPRnDfeH1l503s/8+SxJdmgyUKC3QvKjOpTV6Z/LyRWJZrruBoZnVB1OrL9o/WHEguC+rD+p9udv281KzQ==}
engines: {node: '>=18'}
peerDependencies:
zod: ^3.0.0
@ -124,8 +139,8 @@ packages:
zod:
optional: true
'@ai-sdk/vue@1.1.11':
resolution: {integrity: sha512-Zw8x+CoJLhPAa2Esn8X0OCAtUqoDRZ9eBcuDag8g+fLbmwdHLzUjgh4mgABKmUYXBcCFmrN+SlowzM4X6yprVQ==}
'@ai-sdk/vue@1.1.15':
resolution: {integrity: sha512-HO54RTfT/MbPZd8wO+lnwXLKpu/ys6P733Ui5IqW5AbpRQ79zShLfV3+Wqu5q9rKXO42brwRHZApigC5Ex7zKQ==}
engines: {node: '>=18'}
peerDependencies:
vue: ^3.3.4
@ -851,10 +866,10 @@ packages:
resolution: {integrity: sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==}
engines: {node: '>= 10.0.0'}
'@pinia/nuxt@0.10.0':
resolution: {integrity: sha512-crYQgsqhEnf0HbOuaLYyLR9hyWK2lYUjcCYhFV4vgo3YThrLdnRKfBLa31au1uqcmgDqoX4mEm2OS3DO8GxR7w==}
'@pinia/nuxt@0.10.1':
resolution: {integrity: sha512-xrpkKZHSmshPK6kQzboJ+TZiZ5zj73gBCI5SfiUaJkKKS9gx4B1hLEzJIjxZl0/HS5jRWrIvQ+u9ulvIRlNiow==}
peerDependencies:
pinia: ^3.0.0
pinia: ^3.0.1
'@pkgjs/parseargs@0.11.0':
resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==}
@ -1160,8 +1175,8 @@ packages:
peerDependencies:
vue: ^2.7.0 || ^3.0.0
'@tavily/core@0.0.3':
resolution: {integrity: sha512-17fsQnuxgkUSpKDq0+JWvfy4JZwe/b7/HVb2dqwFKWd5SILR+86OHL5C4TjEu6gO4DT6yWuH7ZNW9s+IlG+K3Q==}
'@tavily/core@0.3.1':
resolution: {integrity: sha512-7jyvPWG4Zjst0s4v0FMLO1f/dfHqs4FnqvKm86zOGYzXxSfxHu0isbLzlwjJad0csYwF0kifdlECTuNouHfr5A==}
'@trysound/sax@0.2.0':
resolution: {integrity: sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==}
@ -1291,8 +1306,8 @@ packages:
'@vue/devtools-api@6.6.4':
resolution: {integrity: sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==}
'@vue/devtools-api@7.7.1':
resolution: {integrity: sha512-Cexc8GimowoDkJ6eNelOPdYIzsu2mgNyp0scOQ3tiaYSb9iok6LOESSsJvHaI+ib3joRfqRJNLkHFjhNuWA5dg==}
'@vue/devtools-api@7.7.2':
resolution: {integrity: sha512-1syn558KhyN+chO5SjlZIwJ8bV/bQ1nOVTG66t2RbG66ZGekyiYNmRO7X9BJCXQqPsFHlnksqvPhce2qpzxFnA==}
'@vue/devtools-core@7.6.8':
resolution: {integrity: sha512-8X4roysTwzQ94o7IobjVcOd1aZF5iunikrMrHPI2uUdigZCi2kFTQc7ffYiFiTNaLElCpjOhCnM7bo7aK1yU7A==}
@ -1302,11 +1317,11 @@ packages:
'@vue/devtools-kit@7.6.8':
resolution: {integrity: sha512-JhJ8M3sPU+v0P2iZBF2DkdmR9L0dnT5RXJabJqX6o8KtFs3tebdvfoXV2Dm3BFuqeECuMJIfF1aCzSt+WQ4wrw==}
'@vue/devtools-kit@7.7.1':
resolution: {integrity: sha512-yhZ4NPnK/tmxGtLNQxmll90jIIXdb2jAhPF76anvn5M/UkZCiLJy28bYgPIACKZ7FCosyKoaope89/RsFJll1w==}
'@vue/devtools-kit@7.7.2':
resolution: {integrity: sha512-CY0I1JH3Z8PECbn6k3TqM1Bk9ASWxeMtTCvZr7vb+CHi+X/QwQm5F1/fPagraamKMAHVfuuCbdcnNg1A4CYVWQ==}
'@vue/devtools-shared@7.7.1':
resolution: {integrity: sha512-BtgF7kHq4BHG23Lezc/3W2UhK2ga7a8ohAIAGJMBr4BkxUFzhqntQtCiuL1ijo2ztWnmusymkirgqUrXoQKumA==}
'@vue/devtools-shared@7.7.2':
resolution: {integrity: sha512-uBFxnp8gwW2vD6FrJB8JZLUzVb6PNRG0B0jBnHsOH8uKyva2qINY8PTF5Te4QlTbMDqU5K6qtJDr6cNsKWhbOA==}
'@vue/reactivity@3.5.13':
resolution: {integrity: sha512-NaCwtw8o48B9I6L1zl2p41OHo/2Z4wqYGGIK1Khu5T7yxrn+ATOixn/Udn2m+6kZKB/J7cuT9DbWWhRxqixACg==}
@ -1416,8 +1431,8 @@ packages:
resolution: {integrity: sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==}
engines: {node: '>= 14'}
ai@4.1.28:
resolution: {integrity: sha512-DrYyVGK6HKx8TVzwuJOhemH9phdcyMfix5d7giyMYH9tu71Cvfs/Hi4zZPs9KPGDJnZYMLJna7CLs6l5CgRW7g==}
ai@4.1.38:
resolution: {integrity: sha512-jveFmoUbAn05B0OHlbIxUyIjVdhzEIwxFP1ZJtugMLXD6800RyhuaEzFRRBUk5WquLT4Hokm9uwAjCDJ7187dw==}
engines: {node: '>=18'}
peerDependencies:
react: ^18 || ^19 || ^19.0.0-rc
@ -1586,6 +1601,14 @@ packages:
magicast:
optional: true
c12@2.0.2:
resolution: {integrity: sha512-NkvlL5CHZt9kPswJYDCUYtTaMt7JOfcpsnNncfj7sWsc13x6Wz+GiTpBtqZOojFlzyTHui8+OAfR6praV6PYaQ==}
peerDependencies:
magicast: ^0.3.5
peerDependenciesMeta:
magicast:
optional: true
cac@6.7.14:
resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==}
engines: {node: '>=8'}
@ -2516,8 +2539,8 @@ packages:
resolution: {integrity: sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==}
engines: {node: '>=0.10.0'}
js-tiktoken@1.0.18:
resolution: {integrity: sha512-hFYx4xYf6URgcttcGvGuOBJhTxPYZ2R5eIesqCaNRJmYH8sNmsfTeWg4yu//7u1VD/qIUkgKJTpGom9oHXmB4g==}
js-tiktoken@1.0.19:
resolution: {integrity: sha512-XC63YQeEcS47Y53gg950xiZ4IWmkfMe4p2V9OSaBt26q+p47WHn18izuXzSclCI73B7yGqtfRsT6jcZQI0y08g==}
js-tokens@4.0.0:
resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==}
@ -3060,6 +3083,9 @@ packages:
pathe@2.0.2:
resolution: {integrity: sha512-15Ztpk+nov8DR524R4BF7uEuzESgzUEAV4Ah7CUMNGXdE5ELuvxElxGXndBl32vMSsWa1jpNf22Z+Er3sKwq+w==}
pathe@2.0.3:
resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==}
perfect-debounce@1.0.0:
resolution: {integrity: sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==}
@ -3077,8 +3103,8 @@ packages:
resolution: {integrity: sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==}
engines: {node: '>=12'}
pinia@3.0.0:
resolution: {integrity: sha512-Go23UsqaeABb4OYNmpDkE9VwDnqmbbjGzWpQhi3xfNkSPO6ZP+Ttt0EMo2J4DHXW+T0l3EqRneeXdyV/oJg/Mg==}
pinia@3.0.1:
resolution: {integrity: sha512-WXglsDzztOTH6IfcJ99ltYZin2mY8XZCXujkYWVIJlBjqsP6ST7zw+Aarh63E1cDVYeyUcPCxPHzJpEOmzB6Wg==}
peerDependencies:
typescript: '>=4.4.4'
vue: ^2.7.0 || ^3.5.11
@ -3799,6 +3825,10 @@ packages:
resolution: {integrity: sha512-y5ZYDG+j7IB45+Y6CIkWIKou4E1JFigCUw6vI+h15HdYAKmT0oQWcawnxXuwJG8srJyXhIZuWz5uXB1MQ/ARZw==}
engines: {node: '>=18.20.6'}
unimport@4.1.1:
resolution: {integrity: sha512-j9+fijH6aDd05yv1fXlyt7HSxtOWtGtrZeYTVBsSUg57Iuf+Ps2itIZjeyu7bEQ4k0WOgYhHrdW8m/pJgOpl5g==}
engines: {node: '>=18.12.0'}
universalify@2.0.1:
resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==}
engines: {node: '>= 10.0.0'}
@ -3819,6 +3849,10 @@ packages:
resolution: {integrity: sha512-unB2e2ogZwEoMw/X0Gq1vj2jaRKLmTh9wcSEJggESPllcrZI68uO7B8ykixbXqsSwG8r9T7qaHZudXIC/3qvhw==}
engines: {node: '>=18.12.0'}
unplugin-utils@0.2.4:
resolution: {integrity: sha512-8U/MtpkPkkk3Atewj1+RcKIjb5WBimZ/WSLhhR3w6SsIj8XJuKTacSP8g+2JhfSGw0Cb125Y+2zA/IzJZDVbhA==}
engines: {node: '>=18.12.0'}
unplugin-vue-components@28.0.0:
resolution: {integrity: sha512-vYe0wSyqTVhyNFIad1iiGyQGhG++tDOMgohqenMDOAooMJP9vvzCdXTqCVx20A0rCQXFNjgoRbSeDAioLPH36Q==}
engines: {node: '>=14'}
@ -3860,6 +3894,10 @@ packages:
resolution: {integrity: sha512-Q3LU0e4zxKfRko1wMV2HmP8lB9KWislY7hxXpxd+lGx0PRInE4vhMBVEZwpdVYHvtqzhSrzuIfErsob6bQfCzw==}
engines: {node: '>=18.12.0'}
unplugin@2.2.0:
resolution: {integrity: sha512-m1ekpSwuOT5hxkJeZGRxO7gXbXT3gF26NjQ7GdVHoLoF8/nopLcd/QfPigpCy7i51oFHiRJg/CyHhj4vs2+KGw==}
engines: {node: '>=18.12.0'}
unstorage@1.14.4:
resolution: {integrity: sha512-1SYeamwuYeQJtJ/USE1x4l17LkmQBzg7deBJ+U9qOBoHo15d1cDxG4jM31zKRgF7pG0kirZy4wVMX6WL6Zoscg==}
peerDependencies:
@ -4228,52 +4266,65 @@ packages:
peerDependencies:
zod: ^3.24.1
zod@3.24.1:
resolution: {integrity: sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A==}
zod@3.24.2:
resolution: {integrity: sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ==}
snapshots:
'@ai-sdk/openai@1.1.9(zod@3.24.1)':
'@ai-sdk/deepseek@0.1.10(zod@3.24.2)':
dependencies:
'@ai-sdk/openai-compatible': 0.1.10(zod@3.24.2)
'@ai-sdk/provider': 1.0.7
'@ai-sdk/provider-utils': 2.1.8(zod@3.24.2)
zod: 3.24.2
'@ai-sdk/openai-compatible@0.1.10(zod@3.24.2)':
dependencies:
'@ai-sdk/provider': 1.0.7
'@ai-sdk/provider-utils': 2.1.6(zod@3.24.1)
zod: 3.24.1
'@ai-sdk/provider-utils': 2.1.8(zod@3.24.2)
zod: 3.24.2
'@ai-sdk/provider-utils@2.1.6(zod@3.24.1)':
'@ai-sdk/openai@1.1.11(zod@3.24.2)':
dependencies:
'@ai-sdk/provider': 1.0.7
'@ai-sdk/provider-utils': 2.1.8(zod@3.24.2)
zod: 3.24.2
'@ai-sdk/provider-utils@2.1.8(zod@3.24.2)':
dependencies:
'@ai-sdk/provider': 1.0.7
eventsource-parser: 3.0.0
nanoid: 3.3.8
secure-json-parse: 2.7.0
optionalDependencies:
zod: 3.24.1
zod: 3.24.2
'@ai-sdk/provider@1.0.7':
dependencies:
json-schema: 0.4.0
'@ai-sdk/react@1.1.11(react@19.0.0)(zod@3.24.1)':
'@ai-sdk/react@1.1.14(react@19.0.0)(zod@3.24.2)':
dependencies:
'@ai-sdk/provider-utils': 2.1.6(zod@3.24.1)
'@ai-sdk/ui-utils': 1.1.11(zod@3.24.1)
'@ai-sdk/provider-utils': 2.1.8(zod@3.24.2)
'@ai-sdk/ui-utils': 1.1.14(zod@3.24.2)
swr: 2.3.2(react@19.0.0)
throttleit: 2.1.0
optionalDependencies:
react: 19.0.0
zod: 3.24.1
zod: 3.24.2
'@ai-sdk/ui-utils@1.1.11(zod@3.24.1)':
'@ai-sdk/ui-utils@1.1.14(zod@3.24.2)':
dependencies:
'@ai-sdk/provider': 1.0.7
'@ai-sdk/provider-utils': 2.1.6(zod@3.24.1)
zod-to-json-schema: 3.24.1(zod@3.24.1)
'@ai-sdk/provider-utils': 2.1.8(zod@3.24.2)
zod-to-json-schema: 3.24.1(zod@3.24.2)
optionalDependencies:
zod: 3.24.1
zod: 3.24.2
'@ai-sdk/vue@1.1.11(vue@3.5.13(typescript@5.7.3))(zod@3.24.1)':
'@ai-sdk/vue@1.1.15(vue@3.5.13(typescript@5.7.3))(zod@3.24.2)':
dependencies:
'@ai-sdk/provider-utils': 2.1.6(zod@3.24.1)
'@ai-sdk/ui-utils': 1.1.11(zod@3.24.1)
'@ai-sdk/provider-utils': 2.1.8(zod@3.24.2)
'@ai-sdk/ui-utils': 1.1.14(zod@3.24.2)
swrv: 1.1.0(vue@3.5.13(typescript@5.7.3))
optionalDependencies:
vue: 3.5.13(typescript@5.7.3)
@ -5036,7 +5087,7 @@ snapshots:
'@nuxt/kit@3.15.4(magicast@0.3.5)':
dependencies:
c12: 2.0.1(magicast@0.3.5)
c12: 2.0.2(magicast@0.3.5)
consola: 3.4.0
defu: 6.1.4
destr: 2.0.3
@ -5047,14 +5098,14 @@ snapshots:
knitwork: 1.2.0
mlly: 1.7.4
ohash: 1.1.4
pathe: 2.0.2
pathe: 2.0.3
pkg-types: 1.3.1
scule: 1.3.0
semver: 7.7.1
std-env: 3.8.0
ufo: 1.5.4
unctx: 2.4.1
unimport: 4.1.0
unimport: 4.1.1
untyped: 1.5.2
transitivePeerDependencies:
- magicast
@ -5342,10 +5393,10 @@ snapshots:
'@parcel/watcher-win32-ia32': 2.5.1
'@parcel/watcher-win32-x64': 2.5.1
'@pinia/nuxt@0.10.0(magicast@0.3.5)(pinia@3.0.0(typescript@5.7.3)(vue@3.5.13(typescript@5.7.3)))':
'@pinia/nuxt@0.10.1(magicast@0.3.5)(pinia@3.0.1(typescript@5.7.3)(vue@3.5.13(typescript@5.7.3)))':
dependencies:
'@nuxt/kit': 3.15.4(magicast@0.3.5)
pinia: 3.0.0(typescript@5.7.3)(vue@3.5.13(typescript@5.7.3))
pinia: 3.0.1(typescript@5.7.3)(vue@3.5.13(typescript@5.7.3))
transitivePeerDependencies:
- magicast
- supports-color
@ -5604,10 +5655,10 @@ snapshots:
'@tanstack/virtual-core': 3.13.0
vue: 3.5.13(typescript@5.7.3)
'@tavily/core@0.0.3':
'@tavily/core@0.3.1':
dependencies:
axios: 1.7.9
js-tiktoken: 1.0.18
js-tiktoken: 1.0.19
transitivePeerDependencies:
- debug
@ -5799,14 +5850,14 @@ snapshots:
'@vue/devtools-api@6.6.4': {}
'@vue/devtools-api@7.7.1':
'@vue/devtools-api@7.7.2':
dependencies:
'@vue/devtools-kit': 7.7.1
'@vue/devtools-kit': 7.7.2
'@vue/devtools-core@7.6.8(vite@6.1.0(@types/node@22.13.1)(jiti@2.4.2)(lightningcss@1.29.1)(terser@5.38.1)(yaml@2.7.0))(vue@3.5.13(typescript@5.7.3))':
dependencies:
'@vue/devtools-kit': 7.6.8
'@vue/devtools-shared': 7.7.1
'@vue/devtools-shared': 7.7.2
mitt: 3.0.1
nanoid: 5.0.9
pathe: 1.1.2
@ -5817,7 +5868,7 @@ snapshots:
'@vue/devtools-kit@7.6.8':
dependencies:
'@vue/devtools-shared': 7.7.1
'@vue/devtools-shared': 7.7.2
birpc: 0.2.19
hookable: 5.5.3
mitt: 3.0.1
@ -5825,9 +5876,9 @@ snapshots:
speakingurl: 14.0.1
superjson: 2.2.2
'@vue/devtools-kit@7.7.1':
'@vue/devtools-kit@7.7.2':
dependencies:
'@vue/devtools-shared': 7.7.1
'@vue/devtools-shared': 7.7.2
birpc: 0.2.19
hookable: 5.5.3
mitt: 3.0.1
@ -5835,7 +5886,7 @@ snapshots:
speakingurl: 14.0.1
superjson: 2.2.2
'@vue/devtools-shared@7.7.1':
'@vue/devtools-shared@7.7.2':
dependencies:
rfdc: 1.4.1
@ -5942,17 +5993,17 @@ snapshots:
agent-base@7.1.3: {}
ai@4.1.28(react@19.0.0)(zod@3.24.1):
ai@4.1.38(react@19.0.0)(zod@3.24.2):
dependencies:
'@ai-sdk/provider': 1.0.7
'@ai-sdk/provider-utils': 2.1.6(zod@3.24.1)
'@ai-sdk/react': 1.1.11(react@19.0.0)(zod@3.24.1)
'@ai-sdk/ui-utils': 1.1.11(zod@3.24.1)
'@ai-sdk/provider-utils': 2.1.8(zod@3.24.2)
'@ai-sdk/react': 1.1.14(react@19.0.0)(zod@3.24.2)
'@ai-sdk/ui-utils': 1.1.14(zod@3.24.2)
'@opentelemetry/api': 1.9.0
jsondiffpatch: 0.6.0
optionalDependencies:
react: 19.0.0
zod: 3.24.1
zod: 3.24.2
ajv@6.12.6:
dependencies:
@ -6126,6 +6177,23 @@ snapshots:
optionalDependencies:
magicast: 0.3.5
c12@2.0.2(magicast@0.3.5):
dependencies:
chokidar: 4.0.3
confbox: 0.1.8
defu: 6.1.4
dotenv: 16.4.7
giget: 1.2.4
jiti: 2.4.2
mlly: 1.7.4
ohash: 1.1.4
pathe: 2.0.3
perfect-debounce: 1.0.0
pkg-types: 1.3.1
rc9: 2.1.2
optionalDependencies:
magicast: 0.3.5
cac@6.7.14: {}
callsites@3.1.0: {}
@ -7055,7 +7123,7 @@ snapshots:
js-levenshtein@1.1.6: {}
js-tiktoken@1.0.18:
js-tiktoken@1.0.19:
dependencies:
base64-js: 1.5.1
@ -7757,6 +7825,8 @@ snapshots:
pathe@2.0.2: {}
pathe@2.0.3: {}
perfect-debounce@1.0.0: {}
performance-now@2.1.0:
@ -7768,9 +7838,9 @@ snapshots:
picomatch@4.0.2: {}
pinia@3.0.0(typescript@5.7.3)(vue@3.5.13(typescript@5.7.3)):
pinia@3.0.1(typescript@5.7.3)(vue@3.5.13(typescript@5.7.3)):
dependencies:
'@vue/devtools-api': 7.7.1
'@vue/devtools-api': 7.7.2
vue: 3.5.13(typescript@5.7.3)
optionalDependencies:
typescript: 5.7.3
@ -7781,7 +7851,7 @@ snapshots:
dependencies:
confbox: 0.1.8
mlly: 1.7.4
pathe: 2.0.2
pathe: 2.0.3
pluralize@8.0.0: {}
@ -8536,6 +8606,23 @@ snapshots:
unplugin: 2.1.2
unplugin-utils: 0.2.3
unimport@4.1.1:
dependencies:
acorn: 8.14.0
escape-string-regexp: 5.0.0
estree-walker: 3.0.3
fast-glob: 3.3.3
local-pkg: 1.0.0
magic-string: 0.30.17
mlly: 1.7.4
pathe: 2.0.3
picomatch: 4.0.2
pkg-types: 1.3.1
scule: 1.3.0
strip-literal: 3.0.0
unplugin: 2.2.0
unplugin-utils: 0.2.4
universalify@2.0.1: {}
unplugin-auto-import@19.0.0(@nuxt/kit@3.15.4(magicast@0.3.5))(@vueuse/core@12.5.0(typescript@5.7.3))(rollup@4.34.6):
@ -8558,6 +8645,11 @@ snapshots:
pathe: 2.0.2
picomatch: 4.0.2
unplugin-utils@0.2.4:
dependencies:
pathe: 2.0.3
picomatch: 4.0.2
unplugin-vue-components@28.0.0(@babel/parser@7.26.8)(@nuxt/kit@3.15.4(magicast@0.3.5))(rollup@4.34.6)(vue@3.5.13(typescript@5.7.3)):
dependencies:
'@antfu/utils': 0.7.10
@ -8637,6 +8729,11 @@ snapshots:
acorn: 8.14.0
webpack-virtual-modules: 0.6.2
unplugin@2.2.0:
dependencies:
acorn: 8.14.0
webpack-virtual-modules: 0.6.2
unstorage@1.14.4(db0@0.2.3)(ioredis@5.5.0):
dependencies:
anymatch: 3.1.3
@ -8941,8 +9038,8 @@ snapshots:
compress-commons: 6.0.2
readable-stream: 4.7.0
zod-to-json-schema@3.24.1(zod@3.24.1):
zod-to-json-schema@3.24.1(zod@3.24.2):
dependencies:
zod: 3.24.1
zod: 3.24.2
zod@3.24.1: {}
zod@3.24.2: {}

View File

@ -1,4 +1,5 @@
import { parsePartialJson } from '@ai-sdk/ui-utils'
import type { TextStreamPart } from 'ai'
import { z } from 'zod'
export type DeepPartial<T> = T extends object
@ -7,6 +8,13 @@ export type DeepPartial<T> = T extends object
: { [P in keyof T]?: DeepPartial<T[P]> }
: T
export type ParseStreamingJsonEvent<T> =
| { type: 'object'; value: DeepPartial<T> }
| { type: 'reasoning'; delta: string }
| { type: 'error'; message: string }
/** The call finished with invalid content that can't be parsed as JSON */
| { type: 'bad-end'; rawText: string }
export function removeJsonMarkdown(text: string) {
text = text.trim()
if (text.startsWith('```json')) {
@ -23,32 +31,56 @@ export function removeJsonMarkdown(text: string) {
}
/**
* 解析流式的 JSON 数据
* @param textStream 字符串流
* @param _schema zod schema 用于类型验证
* @param isValid 自定义验证函数,用于判断解析出的 JSON 是否有效
* @returns 异步生成器yield 解析后的数据
* Parse streaming JSON text
* @param fullStream Returned by AI SDK
* @param _schema zod schema for type definition
* @param isValid Custom validation function to check if the parsed JSON is valid
*/
export async function* parseStreamingJson<T extends z.ZodType>(
textStream: AsyncIterable<string>,
fullStream: AsyncIterable<TextStreamPart<any>>,
_schema: T,
isValid: (value: DeepPartial<z.infer<T>>) => boolean,
): AsyncGenerator<DeepPartial<z.infer<T>>> {
): AsyncGenerator<ParseStreamingJsonEvent<z.infer<T>>> {
let rawText = ''
let isParseSuccessful = false
for await (const chunk of textStream) {
rawText += chunk
const parsed = parsePartialJson(removeJsonMarkdown(rawText))
for await (const chunk of fullStream) {
if (chunk.type === 'reasoning') {
yield { type: 'reasoning', delta: chunk.textDelta }
continue
}
if (chunk.type === 'error') {
yield {
type: 'error',
message:
chunk.error instanceof Error
? chunk.error.message
: String(chunk.error),
}
continue
}
if (chunk.type === 'text-delta') {
rawText += chunk.textDelta
const parsed = parsePartialJson(removeJsonMarkdown(rawText))
isParseSuccessful =
parsed.state === 'repaired-parse' || parsed.state === 'successful-parse'
if (isParseSuccessful && isValid(parsed.value as any)) {
yield parsed.value as DeepPartial<z.infer<T>>
} else {
console.debug(`Failed to parse JSON:`, rawText)
isParseSuccessful =
parsed.state === 'repaired-parse' || parsed.state === 'successful-parse'
if (isParseSuccessful && isValid(parsed.value as any)) {
yield {
type: 'object',
value: parsed.value as DeepPartial<z.infer<T>>,
}
} else {
console.debug(`Failed to parse JSON: ${removeJsonMarkdown(rawText)}`)
}
}
}
return { isSuccessful: isParseSuccessful }
// If the last chunk parses failed, return an error
if (!isParseSuccessful) {
yield {
type: 'bad-end',
rawText,
}
}
}