- {t('settings.general.networkTimeoutHint')} +
+ {networkTimeoutError ?? t('settings.general.networkTimeoutHint')}
@@ -2078,7 +2163,7 @@ function GeneralSettings() { size="sm" variant="secondary" className="min-w-[72px] px-4 whitespace-nowrap" - disabled={!networkDirty || !!networkProxyError || isSavingNetwork} + disabled={!networkDirty || !!networkProxyError || !!networkTimeoutError || isSavingNetwork} loading={isSavingNetwork} onClick={() => void saveNetworkSettings()} > diff --git a/src/server/__tests__/conversations.test.ts b/src/server/__tests__/conversations.test.ts index 11251e8d..16cec1e0 100644 --- a/src/server/__tests__/conversations.test.ts +++ b/src/server/__tests__/conversations.test.ts @@ -2411,6 +2411,55 @@ describe('WebSocket Chat Integration', () => { } }, 20_000) + it('should preserve ChatGPT Official as the active default runtime after restart', async () => { + const providerService = new ProviderService() + await providerService.activateProvider('openai-official') + + const createRes = await fetch(`${baseUrl}/api/sessions`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ workDir: process.cwd() }), + }) + expect(createRes.status).toBe(201) + const { sessionId } = await createRes.json() as { sessionId: string } + + const originalStartSession = conversationService.startSession.bind(conversationService) + const startCalls: Array<{ + sessionId: string + options: { permissionMode?: string; model?: string; effort?: string; providerId?: string | null } | undefined + }> = [] + + conversationService.startSession = (async function patchedStartSession( + sid: string, + workDir: string, + sdkUrl: string, + options?: { permissionMode?: string; model?: string; effort?: string; thinking?: 'enabled' | 'adaptive' | 'disabled'; providerId?: string | null }, + ) { + startCalls.push({ sessionId: sid, options }) + return originalStartSession(sid, workDir, sdkUrl, options) + }) as typeof conversationService.startSession + + try { + const messages = await runTurn(sessionId, 'default ChatGPT Official runtime') + + expect(startCalls).toHaveLength(1) + expect(startCalls[0]).toMatchObject({ + sessionId, + options: { + providerId: 'openai-official', + }, + }) + expect(messages.some((msg) => msg.type === 'message_complete')).toBe(true) + await expect(providerService.listProviders()).resolves.toMatchObject({ + activeId: 'openai-official', + }) + } finally { + conversationService.startSession = originalStartSession + conversationService.stopSession(sessionId) + await providerService.activateOfficial() + } + }, 20_000) + it('should resume streaming to a reconnected client during an active turn', async () => { await withMockStreamDelay(150, async () => { const sessionId = `chat-reconnect-${crypto.randomUUID()}` diff --git a/src/server/ws/handler.ts b/src/server/ws/handler.ts index 0592a06a..f674620a 100644 --- a/src/server/ws/handler.ts +++ b/src/server/ws/handler.ts @@ -17,6 +17,7 @@ import { computerUseApprovalService } from '../services/computerUseApprovalServi import { sessionService } from '../services/sessionService.js' import { SettingsService } from '../services/settingsService.js' import { ProviderService } from '../services/providerService.js' +import { isOpenAIOfficialProviderId } from '../services/openaiOfficialProvider.js' import { diagnosticsService } from '../services/diagnosticsService.js' import { deriveTitle, generateTitle, saveAiTitle } from '../services/titleService.js' import { parseSlashCommand } from '../../utils/slashCommandParsing.js' @@ -1638,12 +1639,22 @@ type RuntimeSettings = { providerId?: string | null } +function isKnownRuntimeProviderId( + providerId: string, + providers: Array<{ id: string }>, +): boolean { + return ( + isOpenAIOfficialProviderId(providerId) || + providers.some((provider) => provider.id === providerId) + ) +} + async function getRuntimeSettings(sessionId?: string): Promise