diff --git a/apps/server/src/services/__test__/providers.test.ts b/apps/server/src/services/__test__/providers.test.ts index a3c0ad8e3..073bd3ab4 100644 --- a/apps/server/src/services/__test__/providers.test.ts +++ b/apps/server/src/services/__test__/providers.test.ts @@ -39,8 +39,9 @@ describe('providerService', () => { expect(result.name).toBe('My OpenAI') const found = await service.findUserConfigById('prov-1') - expect(found?.definitionId).toBe('openai') - expect((found?.config as any).apiKey).toBe('sk-123') + expect(found).toBeDefined() + expect(found!.definitionId).toBe('openai') + expect((found!.config as Record).apiKey).toBe('sk-123') }) it('findUserConfigsByOwnerId should return providers for the user', async () => { diff --git a/packages/stage-ui/src/components/scenes/Stage.vue b/packages/stage-ui/src/components/scenes/Stage.vue index b23a8c262..4261ac6a2 100644 --- a/packages/stage-ui/src/components/scenes/Stage.vue +++ b/packages/stage-ui/src/components/scenes/Stage.vue @@ -163,38 +163,38 @@ function playSpecialToken(special: string) { const lipSyncNode = ref() async function playFunction(item: Parameters>[0]['play']>[0], signal: AbortSignal): Promise { - return new Promise(async (resolve) => { - if (!audioContext) { - resolve() + if (!audioContext || !item.audio) + return + + // Ensure audio context is resumed (browsers suspend it by default until user interaction) + if (audioContext.state === 'suspended') { + try { + await audioContext.resume() + } + catch { return } + } - if (!item.audio) { - resolve() - return - } + const source = audioContext.createBufferSource() + currentAudioSource.value = source + source.buffer = item.audio - // Ensure audio context is resumed (browsers suspend it by default until user interaction) - if (audioContext.state === 'suspended') { - try { - await audioContext.resume() - } - catch { - resolve() + source.connect(audioContext.destination) + if (audioAnalyser.value) + source.connect(audioAnalyser.value) + if (lipSyncNode.value) + source.connect(lipSyncNode.value) + + return new Promise((resolve) => { + let settled = false + const resolveOnce = () => { + if (settled) return - } + settled = true + resolve() } - const source = audioContext.createBufferSource() - currentAudioSource.value = source - source.buffer = item.audio - - source.connect(audioContext.destination) - if (audioAnalyser.value) - source.connect(audioAnalyser.value) - if (lipSyncNode.value) - source.connect(lipSyncNode.value) - const stopPlayback = () => { try { source.stop() @@ -203,7 +203,7 @@ async function playFunction(item: Parameters(async (resolve, reject) => { - try { - const supportedTools = streamOptionsToolsCompatibilityOk(model, chatProvider, messages, options) + const supportedTools = streamOptionsToolsCompatibilityOk(model, chatProvider, messages, options) + const tools = supportedTools + ? [ + ...await mcp(), + ...await debug(), + ...await resolveTools(), + ] + : undefined - await streamText({ + return new Promise((resolve, reject) => { + let settled = false + const resolveOnce = () => { + if (settled) + return + settled = true + resolve() + } + const rejectOnce = (err: unknown) => { + if (settled) + return + settled = true + reject(err) + } + + const onEvent = async (event: unknown) => { + try { + await options?.onStreamEvent?.(event as StreamEvent) + if (event && (event as StreamEvent).type === 'finish') { + const finishReason = (event as any).finishReason + if (finishReason !== 'tool_calls' || !options?.waitForTools) + resolveOnce() + } + else if (event && (event as StreamEvent).type === 'error') { + const error = (event as any).error ?? new Error('Stream error') + rejectOnce(error) + } + } + catch (err) { + rejectOnce(err) + } + } + + try { + streamText({ ...chatProvider.chat(model), maxSteps: 10, messages: sanitized, headers, // TODO: we need Automatic tools discovery - tools: supportedTools - ? [ - ...await mcp(), - ...await debug(), - ...await resolveTools(), - ] - : undefined, - async onEvent(event) { - try { - await options?.onStreamEvent?.(event as StreamEvent) - if (event.type === 'finish' && (event.finishReason !== 'tool_calls' || !options?.waitForTools)) - resolve() - else if (event.type === 'error') - reject(event.error ?? new Error('Stream error')) - } - catch (err) { - reject(err) - } - }, + tools, + onEvent, }) } catch (err) { - reject(err) + rejectOnce(err) } }) } diff --git a/packages/stage-ui/src/tools/mcp.test.ts b/packages/stage-ui/src/tools/mcp.test.ts index e1731c498..385ccde9e 100644 --- a/packages/stage-ui/src/tools/mcp.test.ts +++ b/packages/stage-ui/src/tools/mcp.test.ts @@ -26,8 +26,8 @@ describe('tools mcp schema', () => { const callTool = tools.find(entry => entry.function.name === 'mcp_call_tool') expect(callTool).toBeDefined() - const items = ((callTool?.function.parameters as JsonSchema).properties?.parameters as any)?.items + const items = ((callTool!.function.parameters as JsonSchema).properties?.parameters as JsonSchema)?.items as JsonSchema expect(items).toBeDefined() - expect(items?.additionalProperties).toBe(false) + expect(items.additionalProperties).toBe(false) }) })