feat(qwen-adapter): capture chat SSE and expand tab_container cards
Attach a response listener to the `/api/v2/chat` POST stream, parse its SSE payloads, and reconstruct answer content/extra_info/communication from the `multi_load/iframe` and `bar/iframe` messages. Expand embedded `[(tab_container_N)]` references against the SSE cardMap so nested tab content is inlined instead of leaking a raw placeholder. Prefer the API answer, falling back to page state, and record `response_source` plus `api_event_count` in the raw response for diagnosis. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -3,7 +3,13 @@ import { describe, expect, it } from 'vitest'
|
||||
import type { AdapterContext } from './base'
|
||||
import { __qwenTestUtils, qwenAdapter } from './qwen'
|
||||
|
||||
const { extractQuestionText } = __qwenTestUtils
|
||||
const { extractQuestionText, parseQwenSSEResponse, resolveQwenAnswerText } = __qwenTestUtils
|
||||
|
||||
function buildSSEBody(messages: Record<string, unknown>[]): string {
|
||||
return messages
|
||||
.map((message) => `data:${JSON.stringify({ data: { messages: [message] } })}`)
|
||||
.join('\n\n')
|
||||
}
|
||||
|
||||
describe('qwen adapter helpers', () => {
|
||||
it('extracts question text from monitoring payload', () => {
|
||||
@@ -17,10 +23,55 @@ describe('qwen adapter helpers', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('qwen official SSE response parsing', () => {
|
||||
it('uses the complete API snapshot instead of an earlier incremental snapshot', () => {
|
||||
const parsed = parseQwenSSEResponse(
|
||||
buildSSEBody([
|
||||
{ mime_type: 'multi_load/iframe', status: 'processing', content: '部分' },
|
||||
{ mime_type: 'multi_load/iframe', status: 'complete', content: '完整 API 答案' },
|
||||
]),
|
||||
)
|
||||
|
||||
expect(resolveQwenAnswerText(parsed.content).answerText).toBe('完整 API 答案')
|
||||
expect(parsed.events).toHaveLength(2)
|
||||
})
|
||||
|
||||
it('builds the tab card map from API metadata and expands the selected tab', () => {
|
||||
const parsed = parseQwenSSEResponse(
|
||||
buildSSEBody([
|
||||
{
|
||||
mime_type: 'multi_load/iframe',
|
||||
status: 'complete',
|
||||
content: '[(tab_container_1)]',
|
||||
meta_data: {
|
||||
multi_load: [
|
||||
{
|
||||
source_seq: 'tab_container_1',
|
||||
type: 'tab_container',
|
||||
content: {
|
||||
default_tab: 'answer_2',
|
||||
tabs: [
|
||||
{ tab_id: 'answer_1', content: '备选回答' },
|
||||
{ tab_id: 'answer_2', content: 'API 卡片中的完整回答' },
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
]),
|
||||
)
|
||||
|
||||
expect(resolveQwenAnswerText(parsed.content).answerText).toBe('API 卡片中的完整回答')
|
||||
})
|
||||
})
|
||||
|
||||
describe('qwen adapter query result validation', () => {
|
||||
it('does not submit an internal tab-container placeholder as a successful answer', async () => {
|
||||
it('expands an internal tab-container reference into the selected answer', async () => {
|
||||
const page = {
|
||||
waitForLoadState: async () => undefined,
|
||||
on: () => undefined,
|
||||
off: () => undefined,
|
||||
url: () => 'https://www.qianwen.com/chat/demo',
|
||||
goto: async () => undefined,
|
||||
evaluate: async () => ({
|
||||
@@ -38,8 +89,57 @@ describe('qwen adapter query result validation', () => {
|
||||
content: {
|
||||
multiLoadIframe: {
|
||||
content: '[(tab_container_1)]',
|
||||
cardMap: {
|
||||
tab_container_1: {
|
||||
type: 'tab_container',
|
||||
content: {
|
||||
default_tab: 'answer_2',
|
||||
tabs: [
|
||||
{ tab_id: 'answer_1', content: '备选答案。' },
|
||||
{ tab_id: 'answer_2', content: '这是千问实际展示的完整答案。' },
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
searchResults: [{ url: 'https://example.com/source', title: '来源' }],
|
||||
},
|
||||
extraInfo: null,
|
||||
communication: null,
|
||||
},
|
||||
}),
|
||||
}
|
||||
const context = {
|
||||
playwright: { page, browser: {} },
|
||||
reportProgress: () => undefined,
|
||||
} as unknown as AdapterContext
|
||||
|
||||
const result = await qwenAdapter.query(context, { question_text: '测试问题' })
|
||||
|
||||
expect(result.status).toBe('succeeded')
|
||||
expect(result.payload?.answer).toBe('这是千问实际展示的完整答案。')
|
||||
})
|
||||
|
||||
it('does not submit a tab-container reference when its card data is missing', async () => {
|
||||
const page = {
|
||||
waitForLoadState: async () => undefined,
|
||||
on: () => undefined,
|
||||
off: () => undefined,
|
||||
url: () => 'https://www.qianwen.com/chat/demo',
|
||||
goto: async () => undefined,
|
||||
evaluate: async () => ({
|
||||
ok: true as const,
|
||||
url: 'https://www.qianwen.com/chat/demo',
|
||||
question: {
|
||||
model: 'Qwen',
|
||||
deepSearch: '1',
|
||||
enableSearch: true,
|
||||
cardText: '测试问题',
|
||||
},
|
||||
answer: {
|
||||
reqId: 'req-missing-card',
|
||||
status: 'finish',
|
||||
content: {
|
||||
multiLoadIframe: { content: '[(tab_container_1)]', cardMap: {} },
|
||||
},
|
||||
extraInfo: null,
|
||||
communication: null,
|
||||
@@ -60,6 +160,8 @@ describe('qwen adapter query result validation', () => {
|
||||
it('uses a resolved fallback answer when another content field is still a placeholder', async () => {
|
||||
const page = {
|
||||
waitForLoadState: async () => undefined,
|
||||
on: () => undefined,
|
||||
off: () => undefined,
|
||||
url: () => 'https://www.qianwen.com/chat/demo',
|
||||
goto: async () => undefined,
|
||||
evaluate: async () => ({
|
||||
@@ -93,4 +195,67 @@ describe('qwen adapter query result validation', () => {
|
||||
expect(result.status).toBe('succeeded')
|
||||
expect(result.payload?.answer).toBe('这是完整答案。')
|
||||
})
|
||||
|
||||
it('prefers the official SSE answer over the page-state fallback', async () => {
|
||||
const apiBody = buildSSEBody([
|
||||
{
|
||||
mime_type: 'multi_load/iframe',
|
||||
status: 'complete',
|
||||
content: '官方 SSE 完整答案',
|
||||
},
|
||||
])
|
||||
type MockResponse = {
|
||||
url(): string
|
||||
request(): { method(): string }
|
||||
text(): Promise<string>
|
||||
}
|
||||
let responseListener: ((response: MockResponse) => void) | null = null
|
||||
const page = {
|
||||
waitForLoadState: async () => undefined,
|
||||
on: (_event: string, listener: (response: MockResponse) => void) => {
|
||||
responseListener = listener
|
||||
},
|
||||
off: () => {
|
||||
responseListener = null
|
||||
},
|
||||
url: () => 'https://www.qianwen.com/chat/demo',
|
||||
goto: async () => undefined,
|
||||
evaluate: async () => {
|
||||
responseListener?.({
|
||||
url: () => 'https://chat2.qianwen.com/api/v2/chat',
|
||||
request: () => ({ method: () => 'POST' }),
|
||||
text: async () => apiBody,
|
||||
})
|
||||
return {
|
||||
ok: true as const,
|
||||
url: 'https://www.qianwen.com/chat/demo',
|
||||
question: {
|
||||
model: 'Qwen',
|
||||
deepSearch: '1',
|
||||
enableSearch: true,
|
||||
cardText: '测试问题',
|
||||
},
|
||||
answer: {
|
||||
reqId: 'req-api',
|
||||
status: 'finish',
|
||||
content: { multiLoadIframe: { content: '页面 state 兜底答案' } },
|
||||
extraInfo: null,
|
||||
communication: null,
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
const context = {
|
||||
playwright: { page, browser: {} },
|
||||
reportProgress: () => undefined,
|
||||
} as unknown as AdapterContext
|
||||
|
||||
const result = await qwenAdapter.query(context, { question_text: '测试问题' })
|
||||
|
||||
expect(result.status).toBe('succeeded')
|
||||
expect(result.payload?.answer).toBe('官方 SSE 完整答案')
|
||||
expect(
|
||||
(result.payload?.raw_response_json as Record<string, unknown>).response_source,
|
||||
).toBe('api_sse')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { JsonValue, MonitoringSourceItem } from '@geo/shared-types'
|
||||
import type { Response as PlaywrightResponse } from 'playwright-core'
|
||||
|
||||
import type { MonitorAdapter } from './base'
|
||||
import { normalizeText } from './common'
|
||||
@@ -7,6 +8,7 @@ const QWEN_BOOTSTRAP_URL = 'https://www.qianwen.com/'
|
||||
const QWEN_PAGE_READY_TIMEOUT_MS = 20_000
|
||||
const QWEN_QUERY_TIMEOUT_MS = 90_000
|
||||
const QWEN_INTERNAL_TAB_PLACEHOLDER_PATTERN = /^(?:\[\(tab_container_\d+\)\]\s*)+$/i
|
||||
const QWEN_INTERNAL_TAB_REFERENCE_PATTERN = /\[\(tab_container_\d+\)\]/i
|
||||
|
||||
type QwenPageQuestionSnapshot = {
|
||||
model: string | null
|
||||
@@ -41,6 +43,13 @@ type QwenPageQueryFailureResult = {
|
||||
|
||||
type QwenPageQueryResult = QwenPageQuerySuccessResult | QwenPageQueryFailureResult
|
||||
|
||||
type QwenParsedSSEResponse = {
|
||||
content: Record<string, unknown> | null
|
||||
extraInfo: Record<string, unknown> | null
|
||||
communication: Record<string, unknown> | null
|
||||
events: Record<string, unknown>[]
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
||||
}
|
||||
@@ -206,18 +215,67 @@ function isQwenInternalPlaceholderOnly(value: unknown): boolean {
|
||||
return Boolean(text && QWEN_INTERNAL_TAB_PLACEHOLDER_PATTERN.test(text))
|
||||
}
|
||||
|
||||
function resolveQwenTabContainerText(card: unknown): string | null {
|
||||
if (!isRecord(card) || card.type !== 'tab_container' || !isRecord(card.content)) {
|
||||
return null
|
||||
}
|
||||
|
||||
const tabs = Array.isArray(card.content.tabs)
|
||||
? card.content.tabs.filter(isRecord)
|
||||
: []
|
||||
const selectedTabId =
|
||||
normalizeOptionalString(card.content.selected_tab) ??
|
||||
normalizeOptionalString(card.content.default_tab)
|
||||
const selectedTab = selectedTabId
|
||||
? tabs.find((tab) => normalizeOptionalString(tab.tab_id) === selectedTabId)
|
||||
: null
|
||||
const fallbackTab = tabs.find((tab) => normalizeOptionalString(tab.content))
|
||||
|
||||
return normalizeOptionalString((selectedTab ?? fallbackTab)?.content)
|
||||
}
|
||||
|
||||
function expandQwenTabContainers(content: unknown, value: string): string {
|
||||
if (!isRecord(content) || !isRecord(content.multiLoadIframe)) {
|
||||
return value
|
||||
}
|
||||
|
||||
const cardMap = content.multiLoadIframe.cardMap
|
||||
if (!isRecord(cardMap)) {
|
||||
return value
|
||||
}
|
||||
|
||||
let expanded = value
|
||||
for (let depth = 0; depth < 4; depth += 1) {
|
||||
const next = expanded.replace(/\[\((tab_container_\d+)\)\]/gi, (reference, sourceSeq) => {
|
||||
return resolveQwenTabContainerText(cardMap[sourceSeq]) ?? reference
|
||||
})
|
||||
if (next === expanded) {
|
||||
break
|
||||
}
|
||||
expanded = next
|
||||
}
|
||||
|
||||
return expanded.trim()
|
||||
}
|
||||
|
||||
function resolveQwenAnswerText(content: unknown): {
|
||||
answerText: string | null
|
||||
unresolvedPlaceholder: string | null
|
||||
} {
|
||||
const candidates = [
|
||||
const rawCandidates = [
|
||||
readNestedString(content, ['multiLoadIframe', 'content']),
|
||||
readNestedString(content, ['barIframe', 'content']),
|
||||
]
|
||||
const answerText = candidates.find(
|
||||
(candidate) => candidate && !isQwenInternalPlaceholderOnly(candidate),
|
||||
const candidates = rawCandidates.map((candidate) =>
|
||||
candidate ? expandQwenTabContainers(content, candidate) : null,
|
||||
)
|
||||
const unresolvedPlaceholder = candidates.find(isQwenInternalPlaceholderOnly)
|
||||
const answerText = candidates.find(
|
||||
(candidate) => candidate && !QWEN_INTERNAL_TAB_REFERENCE_PATTERN.test(candidate),
|
||||
)
|
||||
const unresolvedPlaceholder =
|
||||
candidates.find(
|
||||
(candidate) => candidate && QWEN_INTERNAL_TAB_REFERENCE_PATTERN.test(candidate),
|
||||
) ?? rawCandidates.find(isQwenInternalPlaceholderOnly)
|
||||
|
||||
return {
|
||||
answerText: answerText ?? null,
|
||||
@@ -225,6 +283,152 @@ function resolveQwenAnswerText(content: unknown): {
|
||||
}
|
||||
}
|
||||
|
||||
function parseQwenSSEPayloads(body: string): Record<string, unknown>[] {
|
||||
const payloads: Record<string, unknown>[] = []
|
||||
for (const block of body.split(/\r?\n\r?\n/)) {
|
||||
const data = block
|
||||
.split(/\r?\n/)
|
||||
.filter((line) => line.startsWith('data:'))
|
||||
.map((line) => line.slice(5).trimStart())
|
||||
.join('\n')
|
||||
.trim()
|
||||
if (!data || data === '[DONE]') {
|
||||
continue
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(data) as unknown
|
||||
if (isRecord(parsed)) {
|
||||
payloads.push(parsed)
|
||||
}
|
||||
} catch {
|
||||
continue
|
||||
}
|
||||
}
|
||||
return payloads
|
||||
}
|
||||
|
||||
function qwenSSEMessages(payload: Record<string, unknown>): Record<string, unknown>[] {
|
||||
const data = isRecord(payload.data) ? payload.data : payload
|
||||
const messages = Array.isArray(data.messages)
|
||||
? data.messages
|
||||
: Array.isArray(data.response_messages)
|
||||
? data.response_messages
|
||||
: []
|
||||
return messages.filter(isRecord)
|
||||
}
|
||||
|
||||
function selectBestQwenSSEMessage(
|
||||
messages: Record<string, unknown>[],
|
||||
mimeType: string,
|
||||
): Record<string, unknown> | null {
|
||||
let selected: Record<string, unknown> | null = null
|
||||
let selectedScore = -1
|
||||
for (const message of messages) {
|
||||
if (normalizeOptionalString(message.mime_type) !== mimeType) {
|
||||
continue
|
||||
}
|
||||
const contentLength = normalizeOptionalString(message.content)?.length ?? 0
|
||||
const score = (message.status === 'complete' ? 1_000_000 : 0) + contentLength
|
||||
if (score >= selectedScore) {
|
||||
selected = message
|
||||
selectedScore = score
|
||||
}
|
||||
}
|
||||
return selected
|
||||
}
|
||||
|
||||
function buildQwenSSECardMap(messages: Record<string, unknown>[]): Record<string, unknown> {
|
||||
const cardMap: Record<string, unknown> = {}
|
||||
for (const message of messages) {
|
||||
if (normalizeOptionalString(message.mime_type) !== 'multi_load/iframe') {
|
||||
continue
|
||||
}
|
||||
const metaData = isRecord(message.meta_data) ? message.meta_data : null
|
||||
const cards = Array.isArray(metaData?.multi_load) ? metaData.multi_load : []
|
||||
for (const card of cards) {
|
||||
if (!isRecord(card)) {
|
||||
continue
|
||||
}
|
||||
const sourceSeq = normalizeOptionalString(card.source_seq ?? card.sourceSeq)
|
||||
if (!sourceSeq) {
|
||||
continue
|
||||
}
|
||||
cardMap[sourceSeq] = {
|
||||
sourceSeq,
|
||||
type: normalizeOptionalString(card.type),
|
||||
content: card.content ?? null,
|
||||
html: card.html ?? null,
|
||||
htmlMeta: card.html_meta ?? card.htmlMeta ?? null,
|
||||
}
|
||||
}
|
||||
}
|
||||
return cardMap
|
||||
}
|
||||
|
||||
function mergeQwenSSERecord(
|
||||
current: Record<string, unknown> | null,
|
||||
value: unknown,
|
||||
): Record<string, unknown> | null {
|
||||
return isRecord(value) ? { ...(current ?? {}), ...value } : current
|
||||
}
|
||||
|
||||
function parseQwenSSEResponse(body: string): QwenParsedSSEResponse {
|
||||
const payloads = parseQwenSSEPayloads(body)
|
||||
const messages = payloads.flatMap(qwenSSEMessages)
|
||||
const multiLoadMessage = selectBestQwenSSEMessage(messages, 'multi_load/iframe')
|
||||
const barMessage = selectBestQwenSSEMessage(messages, 'bar/iframe')
|
||||
let extraInfo: Record<string, unknown> | null = null
|
||||
let communication: Record<string, unknown> | null = null
|
||||
|
||||
for (const payload of payloads) {
|
||||
const data = isRecord(payload.data) ? payload.data : payload
|
||||
extraInfo = mergeQwenSSERecord(extraInfo, data.extra_info ?? data.extraInfo)
|
||||
communication = mergeQwenSSERecord(
|
||||
communication,
|
||||
data.communication ?? data.communication_info,
|
||||
)
|
||||
}
|
||||
|
||||
const content: Record<string, unknown> = {}
|
||||
if (multiLoadMessage) {
|
||||
content.multiLoadIframe = {
|
||||
status: normalizeOptionalString(multiLoadMessage.status),
|
||||
content: normalizeOptionalString(multiLoadMessage.content),
|
||||
metaData: isRecord(multiLoadMessage.meta_data) ? multiLoadMessage.meta_data : null,
|
||||
cardMap: buildQwenSSECardMap(messages),
|
||||
}
|
||||
}
|
||||
if (barMessage) {
|
||||
content.barIframe = {
|
||||
status: normalizeOptionalString(barMessage.status),
|
||||
content: normalizeOptionalString(barMessage.content),
|
||||
metaData: isRecord(barMessage.meta_data) ? barMessage.meta_data : null,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
content: Object.keys(content).length ? content : null,
|
||||
extraInfo,
|
||||
communication,
|
||||
events: payloads.map((payload) => (isRecord(payload.data) ? payload.data : payload)),
|
||||
}
|
||||
}
|
||||
|
||||
function isQwenChatAPIResponse(response: {
|
||||
url(): string
|
||||
request(): { method(): string }
|
||||
}): boolean {
|
||||
if (response.request().method() !== 'POST') {
|
||||
return false
|
||||
}
|
||||
try {
|
||||
return new URL(response.url()).pathname === '/api/v2/chat'
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
function toJsonValue(value: unknown, depth = 0): JsonValue {
|
||||
if (value == null || depth > 16) {
|
||||
return null
|
||||
@@ -636,12 +840,21 @@ export const qwenAdapter: MonitorAdapter = {
|
||||
}
|
||||
|
||||
context.reportProgress('qwen.query')
|
||||
const pageResult = await page.evaluate(qwenQueryInPage, {
|
||||
questionText,
|
||||
timeoutMs: QWEN_QUERY_TIMEOUT_MS,
|
||||
readyTimeoutMs: QWEN_PAGE_READY_TIMEOUT_MS,
|
||||
deepThink: true,
|
||||
})
|
||||
let apiResponse: PlaywrightResponse | null = null
|
||||
const captureAPIResponse = (response: PlaywrightResponse) => {
|
||||
if (!apiResponse && isQwenChatAPIResponse(response)) {
|
||||
apiResponse = response
|
||||
}
|
||||
}
|
||||
page.on('response', captureAPIResponse)
|
||||
const pageResult = await page
|
||||
.evaluate(qwenQueryInPage, {
|
||||
questionText,
|
||||
timeoutMs: QWEN_QUERY_TIMEOUT_MS,
|
||||
readyTimeoutMs: QWEN_PAGE_READY_TIMEOUT_MS,
|
||||
deepThink: true,
|
||||
})
|
||||
.finally(() => page.off('response', captureAPIResponse))
|
||||
|
||||
if (!pageResult?.ok || !pageResult.answer) {
|
||||
const message = formatQwenQueryError(pageResult)
|
||||
@@ -655,9 +868,18 @@ export const qwenAdapter: MonitorAdapter = {
|
||||
}
|
||||
}
|
||||
|
||||
const { answerText, unresolvedPlaceholder } = resolveQwenAnswerText(
|
||||
pageResult.answer.content,
|
||||
)
|
||||
const capturedAPIResponse = apiResponse as PlaywrightResponse | null
|
||||
const apiResponseBody = capturedAPIResponse
|
||||
? await capturedAPIResponse.text().catch(() => null)
|
||||
: null
|
||||
const apiResult = apiResponseBody ? parseQwenSSEResponse(apiResponseBody) : null
|
||||
|
||||
const apiAnswer = resolveQwenAnswerText(apiResult?.content)
|
||||
const pageAnswer = resolveQwenAnswerText(pageResult.answer.content)
|
||||
const answerText = apiAnswer.answerText ?? pageAnswer.answerText
|
||||
const unresolvedPlaceholder =
|
||||
apiAnswer.unresolvedPlaceholder ?? pageAnswer.unresolvedPlaceholder
|
||||
const responseSource = apiAnswer.answerText ? 'api_sse' : 'page_state'
|
||||
const requestId =
|
||||
normalizeText(pageResult.answer.reqId) ??
|
||||
readNestedString(pageResult.answer.communication, ['reqid']) ??
|
||||
@@ -666,6 +888,7 @@ export const qwenAdapter: MonitorAdapter = {
|
||||
const providerModel = resolveQwenProviderModel(pageResult)
|
||||
|
||||
const searchResults: MonitoringSourceItem[] = []
|
||||
collectQwenSources(apiResult?.events, searchResults)
|
||||
collectQwenSources(pageResult.answer.content, searchResults)
|
||||
const dedupedSearchResults = dedupeSourceItems(searchResults)
|
||||
|
||||
@@ -708,6 +931,8 @@ export const qwenAdapter: MonitorAdapter = {
|
||||
raw_response_json: {
|
||||
platform: 'qwen',
|
||||
mode: pageResult.question?.deepSearch === '1' ? 'deep_think' : 'standard',
|
||||
response_source: responseSource,
|
||||
api_event_count: apiResult?.events.length ?? 0,
|
||||
page_url: pageResult.url,
|
||||
session_id: sessionId,
|
||||
provider_request_id: requestId,
|
||||
@@ -716,9 +941,9 @@ export const qwenAdapter: MonitorAdapter = {
|
||||
question: toJsonValue(pageResult.question),
|
||||
source_count: dedupedSearchResults.length,
|
||||
search_results: toJsonSources(dedupedSearchResults),
|
||||
content: toJsonValue(pageResult.answer.content),
|
||||
extra_info: toJsonValue(pageResult.answer.extraInfo),
|
||||
communication: toJsonValue(pageResult.answer.communication),
|
||||
content: toJsonValue(apiResult?.content ?? pageResult.answer.content),
|
||||
extra_info: toJsonValue(apiResult?.extraInfo ?? pageResult.answer.extraInfo),
|
||||
communication: toJsonValue(apiResult?.communication ?? pageResult.answer.communication),
|
||||
status: 'succeeded',
|
||||
},
|
||||
},
|
||||
@@ -730,4 +955,5 @@ export const __qwenTestUtils = {
|
||||
extractQuestionText,
|
||||
isQwenInternalPlaceholderOnly,
|
||||
resolveQwenAnswerText,
|
||||
parseQwenSSEResponse,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user