Compare commits

...

4 Commits

Author SHA1 Message Date
FFXN
c2ea3d5b44 fix: Some models only support streaming output (e.g. Qwen3 open-source edition), so all chunks must be consumed and concatenated into a single `LLMResult` for compatibility. 2026-03-01 11:37:33 +08:00
FFXN
e900811910 fix: Some models only support streaming output (e.g. Qwen3 open-source edition), so all chunks must be consumed and concatenated into a single `LLMResult` for compatibility. 2026-03-01 11:21:05 +08:00
FFXN
de4dac89ae fix: Fixed the adaptation issue of response formats for different mod… (#32326)
Some checks are pending
autofix.ci / autofix (push) Waiting to run
Build and Push API & Web / build (api, DIFY_API_IMAGE_NAME, linux/amd64, build-api-amd64) (push) Waiting to run
Build and Push API & Web / build (api, DIFY_API_IMAGE_NAME, linux/arm64, build-api-arm64) (push) Waiting to run
Build and Push API & Web / build (web, DIFY_WEB_IMAGE_NAME, linux/amd64, build-web-amd64) (push) Waiting to run
Build and Push API & Web / build (web, DIFY_WEB_IMAGE_NAME, linux/arm64, build-web-arm64) (push) Waiting to run
Build and Push API & Web / create-manifest (api, DIFY_API_IMAGE_NAME, merge-api-images) (push) Blocked by required conditions
Build and Push API & Web / create-manifest (web, DIFY_WEB_IMAGE_NAME, merge-web-images) (push) Blocked by required conditions
Main CI Pipeline / Check Changed Files (push) Waiting to run
Main CI Pipeline / API Tests (push) Blocked by required conditions
Main CI Pipeline / Web Tests (push) Blocked by required conditions
Main CI Pipeline / Style Check (push) Waiting to run
Main CI Pipeline / VDB Tests (push) Blocked by required conditions
Main CI Pipeline / DB Migration Test (push) Blocked by required conditions
2026-03-01 10:23:17 +08:00
Stephen Zhou
eb66d36ea8 chore: add vinext as dev server (#32559)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: yyh <92089059+lyzno1@users.noreply.github.com>
2026-03-01 09:54:39 +08:00
19 changed files with 585 additions and 345 deletions

View File

@@ -83,19 +83,21 @@ def _merge_tool_call_delta(
tool_call.function.arguments += delta.function.arguments
def _build_llm_result_from_first_chunk(
def _build_llm_result_from_chunks(
model: str,
prompt_messages: Sequence[PromptMessage],
chunks: Iterator[LLMResultChunk],
) -> LLMResult:
"""
Build a single `LLMResult` from the first returned chunk.
Build a single `LLMResult` by accumulating all returned chunks.
This is used for `stream=False` because the plugin side may still implement the response via a chunked stream.
Some models only support streaming output (e.g. Qwen3 open-source edition)
and the plugin side may still implement the response via a chunked stream,
so all chunks must be consumed and concatenated into a single ``LLMResult``.
Note:
This function always drains the `chunks` iterator after reading the first chunk to ensure any underlying
streaming resources are released (e.g., HTTP connections owned by the plugin runtime).
The ``usage`` is taken from the last chunk that carries it, which is the
typical convention for streaming responses (the final chunk contains the
aggregated token counts).
"""
content = ""
content_list: list[PromptMessageContentUnionTypes] = []
@@ -104,24 +106,21 @@ def _build_llm_result_from_first_chunk(
tools_calls: list[AssistantPromptMessage.ToolCall] = []
try:
first_chunk = next(chunks, None)
if first_chunk is not None:
if isinstance(first_chunk.delta.message.content, str):
content += first_chunk.delta.message.content
elif isinstance(first_chunk.delta.message.content, list):
content_list.extend(first_chunk.delta.message.content)
for chunk in chunks:
if isinstance(chunk.delta.message.content, str):
content += chunk.delta.message.content
elif isinstance(chunk.delta.message.content, list):
content_list.extend(chunk.delta.message.content)
if first_chunk.delta.message.tool_calls:
_increase_tool_call(first_chunk.delta.message.tool_calls, tools_calls)
if chunk.delta.message.tool_calls:
_increase_tool_call(chunk.delta.message.tool_calls, tools_calls)
usage = first_chunk.delta.usage or LLMUsage.empty_usage()
system_fingerprint = first_chunk.system_fingerprint
finally:
try:
for _ in chunks:
pass
except Exception:
logger.debug("Failed to drain non-stream plugin chunk iterator.", exc_info=True)
if chunk.delta.usage:
usage = chunk.delta.usage
if chunk.system_fingerprint:
system_fingerprint = chunk.system_fingerprint
except Exception:
logger.exception("Error while consuming non-stream plugin chunk iterator.")
return LLMResult(
model=model,
@@ -174,7 +173,7 @@ def _normalize_non_stream_plugin_result(
) -> LLMResult:
if isinstance(result, LLMResult):
return result
return _build_llm_result_from_first_chunk(model=model, prompt_messages=prompt_messages, chunks=result)
return _build_llm_result_from_chunks(model=model, prompt_messages=prompt_messages, chunks=result)
def _increase_tool_call(

View File

@@ -469,7 +469,7 @@ class ParagraphIndexProcessor(BaseIndexProcessor):
if not isinstance(result, LLMResult):
raise ValueError("Expected LLMResult when stream=False")
summary_content = getattr(result.message, "content", "")
summary_content = result.message.get_text_content()
usage = result.usage
# Deduct quota for summary generation (same as workflow nodes)

View File

@@ -28,13 +28,13 @@ import { cn } from '@/utils/classnames'
export type IAppDetailLayoutProps = {
children: React.ReactNode
params: { datasetId: string }
datasetId: string
}
const DatasetDetailLayout: FC<IAppDetailLayoutProps> = (props) => {
const {
children,
params: { datasetId },
datasetId,
} = props
const { t } = useTranslation()
const pathname = usePathname()

View File

@@ -6,12 +6,11 @@ const DatasetDetailLayout = async (
params: Promise<{ datasetId: string }>
},
) => {
const params = await props.params
const {
children,
params,
} = props
return <Main params={(await params)}>{children}</Main>
return <Main datasetId={(await params).datasetId}>{children}</Main>
}
export default DatasetDetailLayout

View File

@@ -1,5 +1,5 @@
import { CodeGroup } from '../code.tsx'
import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from '../md.tsx'
import { Row, Col, Properties, Property, Heading, SubProperty } from '../md.tsx'
# Completion App API

View File

@@ -1,5 +1,5 @@
import { CodeGroup } from '../code.tsx'
import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from '../md.tsx'
import { Row, Col, Properties, Property, Heading, SubProperty } from '../md.tsx'
# Completion アプリ API

View File

@@ -1,5 +1,5 @@
import { CodeGroup, Embed } from '../code.tsx'
import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from '../md.tsx'
import { Row, Col, Properties, Property, Heading, SubProperty } from '../md.tsx'
# Advanced Chat App API

View File

@@ -1,5 +1,5 @@
import { CodeGroup } from '../code.tsx'
import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from '../md.tsx'
import { Row, Col, Properties, Property, Heading, SubProperty } from '../md.tsx'
# 高度なチャットアプリ API

View File

@@ -1,5 +1,5 @@
import { CodeGroup } from '../code.tsx'
import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from '../md.tsx'
import { Row, Col, Properties, Property, Heading, SubProperty } from '../md.tsx'
# Chat App API

View File

@@ -1,5 +1,5 @@
import { CodeGroup } from '../code.tsx'
import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from '../md.tsx'
import { Row, Col, Properties, Property, Heading, SubProperty } from '../md.tsx'
# チャットアプリ API

View File

@@ -1,5 +1,5 @@
import { CodeGroup } from '../code.tsx'
import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from '../md.tsx'
import { Row, Col, Properties, Property, Heading, SubProperty } from '../md.tsx'
# Workflow App API

View File

@@ -1,5 +1,5 @@
import { CodeGroup } from '../code.tsx'
import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from '../md.tsx'
import { Row, Col, Properties, Property, Heading, SubProperty } from '../md.tsx'
# ワークフローアプリ API

View File

@@ -1,5 +1,5 @@
import { CodeGroup } from '../code.tsx'
import { Row, Col, Properties, Property, Heading, SubProperty, Paragraph } from '../md.tsx'
import { Row, Col, Properties, Property, Heading, SubProperty } from '../md.tsx'
# Workflow 应用 API

View File

@@ -1,10 +1,9 @@
import type { AppRouterInstance } from 'next/dist/shared/lib/app-router-context.shared-runtime'
import type { AppContextValue } from '@/context/app-context'
import type { ModalContextState } from '@/context/modal-context'
import type { ProviderContextState } from '@/context/provider-context'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { fireEvent, render, screen, waitFor } from '@testing-library/react'
import { AppRouterContext } from 'next/dist/shared/lib/app-router-context.shared-runtime'
import { useRouter } from 'next/navigation'
import { Plan } from '@/app/components/billing/type'
import { useAppContext } from '@/context/app-context'
import { useGlobalPublicStore } from '@/context/global-public-context'
@@ -50,6 +49,14 @@ vi.mock('@/service/use-common', () => ({
useLogout: vi.fn(),
}))
vi.mock('next/navigation', async (importOriginal) => {
const actual = await importOriginal<typeof import('next/navigation')>()
return {
...actual,
useRouter: vi.fn(),
}
})
vi.mock('@/context/i18n', () => ({
useDocLink: () => (path: string) => `https://docs.dify.ai${path}`,
}))
@@ -119,15 +126,6 @@ describe('AccountDropdown', () => {
const mockSetShowAccountSettingModal = vi.fn()
const renderWithRouter = (ui: React.ReactElement) => {
const mockRouter = {
push: mockPush,
replace: vi.fn(),
prefetch: vi.fn(),
back: vi.fn(),
forward: vi.fn(),
refresh: vi.fn(),
} as unknown as AppRouterInstance
const queryClient = new QueryClient({
defaultOptions: {
queries: {
@@ -138,9 +136,7 @@ describe('AccountDropdown', () => {
return render(
<QueryClientProvider client={queryClient}>
<AppRouterContext.Provider value={mockRouter}>
{ui}
</AppRouterContext.Provider>
{ui}
</QueryClientProvider>,
)
}
@@ -166,6 +162,14 @@ describe('AccountDropdown', () => {
vi.mocked(useLogout).mockReturnValue({
mutateAsync: mockLogout,
} as unknown as ReturnType<typeof useLogout>)
vi.mocked(useRouter).mockReturnValue({
push: mockPush,
replace: vi.fn(),
prefetch: vi.fn(),
back: vi.fn(),
forward: vi.fn(),
refresh: vi.fn(),
})
})
afterEach(() => {

View File

@@ -28,9 +28,12 @@
"scripts": {
"dev": "next dev",
"dev:inspect": "next dev --inspect",
"dev:vinext": "vinext dev",
"build": "next build",
"build:docker": "next build && node scripts/optimize-standalone.js",
"build:vinext": "vinext build",
"start": "node ./scripts/copy-and-start.mjs",
"start:vinext": "vinext start",
"lint": "eslint --cache --concurrency=auto",
"lint:ci": "eslint --cache --concurrency 2",
"lint:fix": "pnpm lint --fix",
@@ -173,6 +176,7 @@
"@iconify-json/ri": "1.2.9",
"@mdx-js/loader": "3.1.1",
"@mdx-js/react": "3.1.1",
"@mdx-js/rollup": "3.1.1",
"@next/eslint-plugin-next": "16.1.6",
"@next/mdx": "16.1.5",
"@rgrove/parse-xml": "4.2.0",
@@ -210,14 +214,15 @@
"@types/uuid": "10.0.0",
"@typescript-eslint/parser": "8.56.1",
"@typescript/native-preview": "7.0.0-dev.20251209.1",
"@vitejs/plugin-react": "5.1.2",
"@vitest/coverage-v8": "4.0.17",
"@vitejs/plugin-react": "5.1.4",
"@vitejs/plugin-rsc": "0.5.21",
"@vitest/coverage-v8": "4.0.18",
"autoprefixer": "10.4.21",
"code-inspector-plugin": "1.3.6",
"cross-env": "10.1.0",
"esbuild": "0.27.2",
"eslint": "10.0.2",
"eslint-plugin-better-tailwindcss": "4.3.1",
"eslint-plugin-better-tailwindcss": "https://pkg.pr.new/hyoban/eslint-plugin-better-tailwindcss@a520d15",
"eslint-plugin-hyoban": "0.11.2",
"eslint-plugin-react-hooks": "7.0.1",
"eslint-plugin-react-refresh": "0.5.2",
@@ -233,6 +238,7 @@
"postcss": "8.5.6",
"postcss-js": "5.0.3",
"react-scan": "0.4.3",
"react-server-dom-webpack": "19.2.4",
"sass": "1.93.2",
"serwist": "9.5.4",
"storybook": "10.2.13",
@@ -240,9 +246,10 @@
"tsx": "4.21.0",
"typescript": "5.9.3",
"uglify-js": "3.19.3",
"vinext": "https://pkg.pr.new/hyoban/vinext@e283197",
"vite": "7.3.1",
"vite-tsconfig-paths": "6.0.4",
"vitest": "4.0.17",
"vite-tsconfig-paths": "6.1.1",
"vitest": "4.0.18",
"vitest-canvas-mock": "1.1.3"
},
"pnpm": {

714
web/pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -13,6 +13,14 @@ const _dirname = typeof __dirname !== 'undefined'
? __dirname
: path.dirname(fileURLToPath(import.meta.url))
const disableSVGOptimize = process.env.TAILWIND_MODE === 'ESLINT'
const svgOptimizeConfig = {
cleanupSVG: !disableSVGOptimize,
deOptimisePaths: !disableSVGOptimize,
runSVGO: !disableSVGOptimize,
parseColors: !disableSVGOptimize,
}
const config = {
theme: {
typography,
@@ -167,11 +175,13 @@ const config = {
source: path.resolve(_dirname, 'app/components/base/icons/assets/public'),
prefix: 'custom-public',
ignoreImportErrors: true,
...svgOptimizeConfig,
}),
...importSvgCollections({
source: path.resolve(_dirname, 'app/components/base/icons/assets/vender'),
prefix: 'custom-vender',
ignoreImportErrors: true,
...svgOptimizeConfig,
}),
},
extraProperties: {

View File

@@ -1,16 +1,60 @@
import type { Plugin } from 'vite'
import path from 'node:path'
import { fileURLToPath } from 'node:url'
import react from '@vitejs/plugin-react'
import vinext from 'vinext'
import { defineConfig } from 'vite'
import tsconfigPaths from 'vite-tsconfig-paths'
const __dirname = path.dirname(fileURLToPath(import.meta.url))
const isCI = !!process.env.CI
export default defineConfig({
plugins: [tsconfigPaths(), react()],
resolve: {
alias: {
'~@': __dirname,
export default defineConfig(({ mode }) => {
return {
plugins: mode === 'test'
? [
tsconfigPaths(),
react(),
{
// Stub .mdx files so components importing them can be unit-tested
name: 'mdx-stub',
enforce: 'pre',
transform(_, id) {
if (id.endsWith('.mdx'))
return { code: 'export default () => null', map: null }
},
} as Plugin,
]
: [
vinext(),
],
resolve: {
alias: {
'~@': __dirname,
},
},
},
// vinext related config
...(mode !== 'test'
? {
optimizeDeps: {
exclude: ['nuqs'],
},
server: {
port: 3000,
},
}
: {}),
// Vitest config
test: {
environment: 'jsdom',
globals: true,
setupFiles: ['./vitest.setup.ts'],
coverage: {
provider: 'v8',
reporter: isCI ? ['json', 'json-summary'] : ['text', 'json', 'json-summary'],
},
},
}
})

View File

@@ -1,27 +0,0 @@
import { defineConfig, mergeConfig } from 'vitest/config'
import viteConfig from './vite.config'
const isCI = !!process.env.CI
export default mergeConfig(viteConfig, defineConfig({
plugins: [
{
// Stub .mdx files so components importing them can be unit-tested
name: 'mdx-stub',
enforce: 'pre',
transform(_, id) {
if (id.endsWith('.mdx'))
return { code: 'export default () => null', map: null }
},
},
],
test: {
environment: 'jsdom',
globals: true,
setupFiles: ['./vitest.setup.ts'],
coverage: {
provider: 'v8',
reporter: isCI ? ['json', 'json-summary'] : ['text', 'json', 'json-summary'],
},
},
}))