diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 0ff1c21..2b02c11 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -34,7 +34,7 @@ jobs:
- name: Setup Node.js
uses: actions/setup-node@v4
with:
- node-version: 18
+ node-version: 22
cache: 'npm'
- name: Set up Python (for native modules)
diff --git a/build/entitlements.mac.plist b/build/entitlements.mac.plist
new file mode 100644
index 0000000..281212c
--- /dev/null
+++ b/build/entitlements.mac.plist
@@ -0,0 +1,14 @@
+
+
+
+
+ com.apple.security.cs.allow-jit
+
+ com.apple.security.cs.allow-unsigned-executable-memory
+
+ com.apple.security.cs.disable-library-validation
+
+ com.apple.security.device.audio-input
+
+
+
diff --git a/electron-builder.json5 b/electron-builder.json5
index a45ee5a..7a4c78f 100644
--- a/electron-builder.json5
+++ b/electron-builder.json5
@@ -40,6 +40,12 @@
mac: {
target: ['dmg'],
icon: 'public/app_logo.png',
+ entitlements: 'build/entitlements.mac.plist',
+ entitlementsInherit: 'build/entitlements.mac.plist',
+ extendInfo: {
+ NSMicrophoneUsageDescription:
+ 'Alice uses the microphone for voice activity detection and speech recognition.',
+ },
artifactName: 'Alice-AI-App-Mac-${version}-Installer.${ext}',
},
win: {
diff --git a/electron/main/settingsManager.ts b/electron/main/settingsManager.ts
index b91b790..31cec4f 100644
--- a/electron/main/settingsManager.ts
+++ b/electron/main/settingsManager.ts
@@ -67,7 +67,20 @@ export interface AppSettings {
SUMMARIZATION_MODEL?: string
SUMMARIZATION_SYSTEM_PROMPT?: string
ttsProvider?: 'openai' | 'local'
- ttsVoice?: 'alloy' | 'echo' | 'fable' | 'nova' | 'onyx' | 'shimmer'
+ ttsVoice?:
+ | 'alloy'
+ | 'ash'
+ | 'ballad'
+ | 'coral'
+ | 'echo'
+ | 'fable'
+ | 'nova'
+ | 'onyx'
+ | 'sage'
+ | 'shimmer'
+ | 'verse'
+ | 'marin'
+ | 'cedar'
localTtsVoice?: string
embeddingProvider?: 'openai' | 'local'
ragEnabled?: boolean
diff --git a/package-lock.json b/package-lock.json
index 3519f65..e504da6 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -44,7 +44,7 @@
"dotenv": "^17.4.2",
"electron": "43.2.0",
"electron-builder": "26.15.3",
- "openai": "^6.33.0",
+ "openai": "^7.4.0",
"pinia": "^3.0.3",
"postcss": "^8.5.25",
"prettier": "^3.9.6",
@@ -58,6 +58,9 @@
"vue": "^3.5.26",
"vue-tsc": "^2.0.6"
},
+ "engines": {
+ "node": ">=22.0.0"
+ },
"optionalDependencies": {
"@rollup/rollup-linux-x64-gnu": "4.62.4"
}
@@ -6892,19 +6895,31 @@
}
},
"node_modules/openai": {
- "version": "6.33.0",
- "resolved": "https://registry.npmjs.org/openai/-/openai-6.33.0.tgz",
- "integrity": "sha512-xAYN1W3YsDXJWA5F277135YfkEk6H7D3D6vWwRhJ3OEkzRgcyK8z/P5P9Gyi/wB4N8kK9kM5ZjprfvyHagKmpw==",
+ "version": "7.4.0",
+ "resolved": "https://registry.npmjs.org/openai/-/openai-7.4.0.tgz",
+ "integrity": "sha512-+C9Muit5x8j9R8ej8ZzVgKcrVDtqFqTy9gxFdov0EItLgU68zrJtF9ZeT0cyqJQW9S3PCJkdFgADtRGquRBtew==",
"dev": true,
"license": "Apache-2.0",
- "bin": {
- "openai": "bin/cli"
+ "engines": {
+ "node": ">=22.0.0"
},
"peerDependencies": {
+ "@aws-sdk/credential-provider-node": ">=3.972.0 <4",
+ "@smithy/hash-node": ">=4.3.0 <5",
+ "@smithy/signature-v4": ">=5.4.0 <6",
"ws": "^8.18.0",
"zod": "^3.25 || ^4.0"
},
"peerDependenciesMeta": {
+ "@aws-sdk/credential-provider-node": {
+ "optional": true
+ },
+ "@smithy/hash-node": {
+ "optional": true
+ },
+ "@smithy/signature-v4": {
+ "optional": true
+ },
"ws": {
"optional": true
},
diff --git a/package.json b/package.json
index 0b266f3..4f89050 100644
--- a/package.json
+++ b/package.json
@@ -6,6 +6,9 @@
"author": "pmbstyle",
"license": "MIT",
"private": true,
+ "engines": {
+ "node": ">=22.0.0"
+ },
"keywords": [
"electron",
"rollup",
@@ -46,7 +49,7 @@
"dotenv": "^17.4.2",
"electron": "43.2.0",
"electron-builder": "26.15.3",
- "openai": "^6.33.0",
+ "openai": "^7.4.0",
"pinia": "^3.0.3",
"postcss": "^8.5.25",
"prettier": "^3.9.6",
diff --git a/src/components/settings/AssistantSettingsTab.vue b/src/components/settings/AssistantSettingsTab.vue
index 8cfc905..83a3ca9 100644
--- a/src/components/settings/AssistantSettingsTab.vue
+++ b/src/components/settings/AssistantSettingsTab.vue
@@ -326,7 +326,7 @@
Model used for generating conversation summaries (e.g.,
- gpt-4.1-nano).
+ gpt-5.6-luna).
diff --git a/src/components/settings/CoreSettingsTab.vue b/src/components/settings/CoreSettingsTab.vue
index 87d05b1..c10285a 100644
--- a/src/components/settings/CoreSettingsTab.vue
+++ b/src/components/settings/CoreSettingsTab.vue
@@ -477,11 +477,18 @@
class="select select-bordered w-full focus:select-primary"
>
+
+
+
+
+
+
+
diff --git a/src/components/wizard/OnboardingWizard.vue b/src/components/wizard/OnboardingWizard.vue
index 9f99c61..a161520 100644
--- a/src/components/wizard/OnboardingWizard.vue
+++ b/src/components/wizard/OnboardingWizard.vue
@@ -92,7 +92,7 @@ import { listZAIModelsForConfig } from '../../services/llmProviders/zai'
const step = ref(1)
const settingsStore = useSettingsStore()
const scrollContainer = ref
()
-const OPENAI_SUMMARIZATION_MODEL = 'gpt-4.1-nano'
+const OPENAI_SUMMARIZATION_MODEL = 'gpt-5.6-luna'
const DEFAULT_MAIN_WINDOW_SIZE = {
width: 500,
height: 500,
diff --git a/src/modules/conversation/__tests__/chatOrchestrator.test.ts b/src/modules/conversation/__tests__/chatOrchestrator.test.ts
index 5c0afc7..f209ea6 100644
--- a/src/modules/conversation/__tests__/chatOrchestrator.test.ts
+++ b/src/modules/conversation/__tests__/chatOrchestrator.test.ts
@@ -111,6 +111,21 @@ describe('chatOrchestrator', () => {
expect(dependencies.setAudioState).toHaveBeenCalledWith('IDLE')
})
+ it.each(['AbortError', 'APIUserAbortError'])(
+ 'silently handles %s from the OpenAI stream request',
+ async errorName => {
+ const error = Object.assign(new Error('Request was aborted.'), {
+ name: errorName,
+ })
+ const { dependencies, orchestrator } = setup({ throwError: error })
+
+ await orchestrator.runChat()
+
+ expect(dependencies.logError).not.toHaveBeenCalled()
+ expect(dependencies.handleStreamError).not.toHaveBeenCalled()
+ }
+ )
+
it('skips work when store is not initialized', async () => {
const ctx = setup()
ctx.dependencies.isInitialized.mockReturnValue(false)
diff --git a/src/modules/conversation/__tests__/speechQueue.test.ts b/src/modules/conversation/__tests__/speechQueue.test.ts
index 43ff66c..6b13b24 100644
--- a/src/modules/conversation/__tests__/speechQueue.test.ts
+++ b/src/modules/conversation/__tests__/speechQueue.test.ts
@@ -63,21 +63,24 @@ describe('createSpeechQueueManager', () => {
expect(deps.setAudioState).not.toHaveBeenCalled()
})
- it('silently handles AbortError from TTS stream', async () => {
- const abortError = Object.assign(new Error('cancel'), {
- name: 'AbortError',
- })
- const deps = buildDependencies({
- ttsStream: vi.fn(async () => {
- throw abortError
- }),
- })
- const manager = createSpeechQueueManager(deps)
-
- await manager.enqueueSpeech('Hello')
-
- expect(deps.logError).not.toHaveBeenCalled()
- })
+ it.each(['AbortError', 'APIUserAbortError'])(
+ 'silently handles %s from TTS stream',
+ async errorName => {
+ const abortError = Object.assign(new Error('cancel'), {
+ name: errorName,
+ })
+ const deps = buildDependencies({
+ ttsStream: vi.fn(async () => {
+ throw abortError
+ }),
+ })
+ const manager = createSpeechQueueManager(deps)
+
+ await manager.enqueueSpeech('Hello')
+
+ expect(deps.logError).not.toHaveBeenCalled()
+ }
+ )
it('does not enqueue a response that resolves after cancellation', async () => {
let resolveTts: ((response: Response) => void) | undefined
diff --git a/src/modules/conversation/__tests__/streamHandler.test.ts b/src/modules/conversation/__tests__/streamHandler.test.ts
index 74281f5..a342e95 100644
--- a/src/modules/conversation/__tests__/streamHandler.test.ts
+++ b/src/modules/conversation/__tests__/streamHandler.test.ts
@@ -170,22 +170,24 @@ describe('createStreamHandler', () => {
expect(spies.handleStreamError).toHaveBeenCalledWith(error)
})
- it('returns early without error when the stream is aborted', async () => {
- const { deps, spies } = createDependencies()
- const handler = createStreamHandler(deps)
-
- const abortingStream: AsyncIterable = {
- async *[Symbol.asyncIterator]() {
- const abortError = new Error('aborted')
- abortError.name = 'AbortError'
- throw abortError
- },
- }
+ it.each(['AbortError', 'APIUserAbortError'])(
+ 'returns early without error for %s',
+ async errorName => {
+ const { deps, spies } = createDependencies()
+ const handler = createStreamHandler(deps)
+
+ const abortingStream: AsyncIterable = {
+ async *[Symbol.asyncIterator]() {
+ const abortError = new Error('aborted')
+ abortError.name = errorName
+ throw abortError
+ },
+ }
- const result = await handler.process({ stream: abortingStream })
+ const result = await handler.process({ stream: abortingStream })
- expect(result.streamEndedNormally).toBe(false)
- expect(spies.handleStreamError).not.toHaveBeenCalled()
- })
+ expect(result.streamEndedNormally).toBe(false)
+ expect(spies.handleStreamError).not.toHaveBeenCalled()
+ }
+ )
})
-
diff --git a/src/modules/conversation/chatOrchestrator.ts b/src/modules/conversation/chatOrchestrator.ts
index eda369f..4d17d21 100644
--- a/src/modules/conversation/chatOrchestrator.ts
+++ b/src/modules/conversation/chatOrchestrator.ts
@@ -1,6 +1,7 @@
import type OpenAI from 'openai'
import type { ChatMessage } from '../../types/chat'
import type { RagSearchResult } from '../../types/rag'
+import { isExpectedAbortError } from '../../utils/isAbortError'
export interface ChatDependencies {
isInitialized(): boolean
@@ -453,7 +454,7 @@ export function createChatOrchestrator(
)
await dependencies.processStream(streamResult, placeholderTempId, false)
} catch (error: any) {
- if (error?.name === 'AbortError') return
+ if (abortController.signal.aborted || isExpectedAbortError(error)) return
dependencies.logError('Error starting OpenAI response stream:', error)
diff --git a/src/modules/conversation/reminderHandler.ts b/src/modules/conversation/reminderHandler.ts
index c30d288..c7b0742 100644
--- a/src/modules/conversation/reminderHandler.ts
+++ b/src/modules/conversation/reminderHandler.ts
@@ -1,4 +1,5 @@
import type { ChatMessage } from '../../types/chat'
+import { isExpectedAbortError } from '../../utils/isAbortError'
export interface ReminderData {
message: string
@@ -46,7 +47,7 @@ export function createReminderHandler(
await dependencies.enqueueSpeech(data.message)
}
} catch (error: any) {
- if (error?.name === 'AbortError') {
+ if (isExpectedAbortError(error)) {
return
}
dependencies.logError(
@@ -64,4 +65,3 @@ export function createReminderHandler(
},
}
}
-
diff --git a/src/modules/conversation/speechQueue.ts b/src/modules/conversation/speechQueue.ts
index 4b1564b..0d87ce7 100644
--- a/src/modules/conversation/speechQueue.ts
+++ b/src/modules/conversation/speechQueue.ts
@@ -1,4 +1,5 @@
import type { AudioState } from '../../stores/generalStore'
+import { isExpectedAbortError } from '../../utils/isAbortError'
export interface SpeechQueueDependencies {
createAbortController(): AbortController
@@ -42,7 +43,7 @@ export function createSpeechQueueManager(
dependencies.setAudioState('SPEAKING')
}
} catch (error: any) {
- if (error?.name === 'AbortError') {
+ if (abortController.signal.aborted || isExpectedAbortError(error)) {
return
}
dependencies.logError('TTS stream creation failed:', error)
diff --git a/src/modules/conversation/streamHandler.ts b/src/modules/conversation/streamHandler.ts
index 3b9f2c6..60f57b3 100644
--- a/src/modules/conversation/streamHandler.ts
+++ b/src/modules/conversation/streamHandler.ts
@@ -4,6 +4,7 @@ import type {
StreamProcessingOptions,
StreamProcessingResult,
} from './types'
+import { isExpectedAbortError } from '../../utils/isAbortError'
const SENTENCE_END_REGEX = /[.!?]\s*$/
@@ -139,7 +140,7 @@ export function createStreamHandler(
await flushSentence()
} catch (error: any) {
- if (error?.name === 'AbortError') {
+ if (isExpectedAbortError(error)) {
return { streamEndedNormally: false }
}
streamEndedNormally = false
diff --git a/src/modules/conversation/toolCallHandler.ts b/src/modules/conversation/toolCallHandler.ts
index f276438..18083b1 100644
--- a/src/modules/conversation/toolCallHandler.ts
+++ b/src/modules/conversation/toolCallHandler.ts
@@ -1,18 +1,11 @@
import type OpenAI from 'openai'
import type { ToolCallHandler, ToolCallHandlerDependencies } from './types'
import type { AudioState } from '../../stores/generalStore'
+import { isExpectedAbortError } from '../../utils/isAbortError'
const PREVIOUS_RESPONSE_NOT_FOUND = 'Previous response with id'
const NOT_FOUND_SUFFIX = 'not found'
-function isAbortError(error: unknown): boolean {
- return (
- !!error &&
- typeof error === 'object' &&
- (error as { name?: string }).name === 'AbortError'
- )
-}
-
function isPreviousResponseMissingError(error: unknown): boolean {
if (!error || typeof error !== 'object') return false
const message = (error as { message?: string }).message || ''
@@ -87,7 +80,7 @@ export function createToolCallHandler(
await attemptContinuation(originalResponseIdForTool)
return
} catch (error) {
- if (isAbortError(error)) {
+ if (isExpectedAbortError(error)) {
return
}
@@ -109,7 +102,7 @@ export function createToolCallHandler(
await attemptContinuation(null)
return
} catch (retryError) {
- if (!isAbortError(retryError)) {
+ if (!isExpectedAbortError(retryError)) {
dependencies.logError(
'[Error Recovery] Retry also failed:',
retryError
diff --git a/src/services/apiService.ts b/src/services/apiService.ts
index 56853d9..da61747 100644
--- a/src/services/apiService.ts
+++ b/src/services/apiService.ts
@@ -369,7 +369,7 @@ const fallbackToOpenAITTS = async (
return openai.audio.speech.create(
{
- model: 'tts-1',
+ model: 'gpt-4o-mini-tts',
voice: settings.ttsVoice || 'nova',
input: text,
response_format: 'mp3',
@@ -801,7 +801,9 @@ export const createSummarizationResponse = async (
...(summarizationModel.startsWith('gpt-5')
? {
reasoning: {
- effort: 'minimal',
+ effort: summarizationModel.startsWith('gpt-5.6')
+ ? 'none'
+ : 'minimal',
},
text: {
verbosity: 'low',
@@ -871,7 +873,9 @@ export const createContextAnalysisResponse = async (
...(analysisModel.startsWith('gpt-5')
? {
reasoning: {
- effort: 'minimal',
+ effort: analysisModel.startsWith('gpt-5.6')
+ ? 'none'
+ : 'minimal',
},
text: {
verbosity: 'low',
diff --git a/src/services/llmProviders/streamAdapters.ts b/src/services/llmProviders/streamAdapters.ts
index 08a24eb..d12997d 100644
--- a/src/services/llmProviders/streamAdapters.ts
+++ b/src/services/llmProviders/streamAdapters.ts
@@ -1,6 +1,4 @@
-function isAbortError(error: any): boolean {
- return error?.name === 'AbortError'
-}
+import { isExpectedAbortError } from '../../utils/isAbortError'
export async function* convertLocalLLMStreamToResponsesFormat(
stream: any,
@@ -190,7 +188,7 @@ export async function* convertLocalLLMStreamToResponsesFormat(
}
}
} catch (error) {
- if (isAbortError(error)) {
+ if (isExpectedAbortError(error)) {
throw error
}
@@ -517,7 +515,7 @@ export async function* convertOpenRouterStreamToResponsesFormat(stream: any) {
}
}
} catch (error) {
- if (isAbortError(error)) {
+ if (isExpectedAbortError(error)) {
throw error
}
diff --git a/src/stores/conversationStore.ts b/src/stores/conversationStore.ts
index e24e1d7..f608056 100644
--- a/src/stores/conversationStore.ts
+++ b/src/stores/conversationStore.ts
@@ -7,6 +7,7 @@ import type { AudioState } from './generalStore'
import { useSettingsStore } from './settingsStore'
import { executeFunction } from '../utils/functionCaller'
import eventBus from '../utils/eventBus'
+import { isExpectedAbortError } from '../utils/isAbortError'
import { createStreamHandler } from '../modules/conversation/streamHandler'
import { createToolCallHandler } from '../modules/conversation/toolCallHandler'
import { createApiInputBuilder } from '../modules/conversation/apiInputBuilder'
@@ -408,7 +409,7 @@ export const useConversationStore = defineStore('conversation', () => {
}
}
} catch (error: any) {
- if (error.name !== 'AbortError') {
+ if (!isExpectedAbortError(error)) {
console.error(
'[ConversationStore] Failed to speak scheduler reminder:',
error
@@ -776,7 +777,7 @@ export const useConversationStore = defineStore('conversation', () => {
setAudioState: (state: string) => setAudioState(state as AudioState),
getAudioState: () => audioState.value,
handleStreamError: (error: unknown) => {
- if ((error as any)?.name === 'AbortError') return
+ if (isExpectedAbortError(error)) return
console.error('Error processing stream:', error)
},
}
@@ -994,7 +995,7 @@ export const useConversationStore = defineStore('conversation', () => {
)
await processStream(streamResult, placeholderTempId, false)
} catch (error: any) {
- if (error.name !== 'AbortError') {
+ if (!isExpectedAbortError(error)) {
console.error('Error starting OpenAI response stream:', error)
if (
diff --git a/src/stores/generalStore.ts b/src/stores/generalStore.ts
index 1f5fce8..2748bac 100644
--- a/src/stores/generalStore.ts
+++ b/src/stores/generalStore.ts
@@ -1,6 +1,7 @@
import { ref, watch } from 'vue'
import { defineStore } from 'pinia'
import eventBus from '../utils/eventBus'
+import { isExpectedAbortError } from '../utils/isAbortError'
import type { ChatMessage } from '../types/chat'
import { createHistoryManager } from '../modules/conversation/historyManager'
import { useCustomAvatarsStore } from './customAvatarsStore'
@@ -153,9 +154,11 @@ export const useGeneralStore = defineStore('general', () => {
videoSource.value = newSource
aiVideo.value.load()
}
- aiVideo.value
- .play()
- .catch(e => console.warn('Video play interrupted or failed:', e))
+ aiVideo.value.play().catch(error => {
+ if (!isExpectedAbortError(error)) {
+ console.warn('Video play failed:', error)
+ }
+ })
}
const handleVideoLoadedData = () => {
diff --git a/src/stores/settingsStore.ts b/src/stores/settingsStore.ts
index 37ca5d8..32001c4 100644
--- a/src/stores/settingsStore.ts
+++ b/src/stores/settingsStore.ts
@@ -67,7 +67,20 @@ export interface AliceSettings {
SUMMARIZATION_MODEL: string
SUMMARIZATION_SYSTEM_PROMPT: string
ttsProvider: 'openai' | 'google' | 'local'
- ttsVoice: 'alloy' | 'echo' | 'fable' | 'nova' | 'onyx' | 'shimmer'
+ ttsVoice:
+ | 'alloy'
+ | 'ash'
+ | 'ballad'
+ | 'coral'
+ | 'echo'
+ | 'fable'
+ | 'nova'
+ | 'onyx'
+ | 'sage'
+ | 'shimmer'
+ | 'verse'
+ | 'marin'
+ | 'cedar'
googleTtsVoice: string
localTtsVoice: string
embeddingProvider: 'openai' | 'local'
@@ -168,7 +181,7 @@ const defaultSettings: AliceSettings = {
mcpServersConfig: '[]',
MAX_HISTORY_MESSAGES_FOR_API: 10,
SUMMARIZATION_MESSAGE_COUNT: 20,
- SUMMARIZATION_MODEL: 'gpt-4.1-nano',
+ SUMMARIZATION_MODEL: 'gpt-5.6-luna',
SUMMARIZATION_SYSTEM_PROMPT: DEFAULT_SUMMARIZATION_SYSTEM_PROMPT,
ttsProvider: 'openai',
ttsVoice: 'nova',
diff --git a/src/utils/isAbortError.ts b/src/utils/isAbortError.ts
new file mode 100644
index 0000000..bfec355
--- /dev/null
+++ b/src/utils/isAbortError.ts
@@ -0,0 +1,10 @@
+const EXPECTED_ABORT_ERROR_NAMES = new Set(['AbortError', 'APIUserAbortError'])
+
+export function isExpectedAbortError(error: unknown): boolean {
+ if (!error || typeof error !== 'object') {
+ return false
+ }
+
+ const name = (error as { name?: unknown }).name
+ return typeof name === 'string' && EXPECTED_ABORT_ERROR_NAMES.has(name)
+}