From 2e48bbe517e4500878f4368727834ad30c82b1a7 Mon Sep 17 00:00:00 2001 From: Ramin Tadayon Date: Tue, 23 Sep 2025 15:14:06 -0700 Subject: [PATCH] Fixes some AI cancellation cases being treated as errors --- src/plus/ai/aiProviderService.ts | 24 ++++++++++++++++++--- src/plus/ai/openAICompatibleProviderBase.ts | 11 ++++++++-- src/plus/ai/openRouterProvider.ts | 11 +++++++++- src/plus/ai/utils/-webview/ai.utils.ts | 11 ++++++++-- 4 files changed, 49 insertions(+), 8 deletions(-) diff --git a/src/plus/ai/aiProviderService.ts b/src/plus/ai/aiProviderService.ts index 3ff30b0865772..7927e5391e834 100644 --- a/src/plus/ai/aiProviderService.ts +++ b/src/plus/ai/aiProviderService.ts @@ -25,6 +25,7 @@ import { AINoRequestDataError, AuthenticationRequiredError, CancellationError, + isCancellationError, } from '../../errors'; import type { AIFeatures } from '../../features'; import { isAdvancedFeature } from '../../features'; @@ -1477,7 +1478,7 @@ export class AIProviderService implements Disposable { const model = await this.getModel(undefined, source); if (model == null || options?.cancellation?.isCancellationRequested) { options?.generating?.cancel(); - return undefined; + return 'cancelled'; } const promise = this.sendRequestWithModel( @@ -1522,7 +1523,7 @@ export class AIProviderService implements Disposable { const model = await this.getModel(undefined, source); if (model == null || options?.cancellation?.isCancellationRequested) { options?.generating?.cancel(); - return undefined; + return 'cancelled'; } return this.sendRequestWithModel( @@ -1618,7 +1619,24 @@ export class AIProviderService implements Disposable { return 'cancelled'; } - const apiKey = await this._provider!.getApiKey(false); + let apiKey: string | undefined; + try { + apiKey = await this._provider!.getApiKey(false); + } catch (ex) { + if (isCancellationError(ex)) { + setLogScopeExit(scope, `model: ${model.provider.id}/${model.id}`, 'cancelled: user cancelled'); + this.container.telemetry.sendEvent( + telementry.key, + { ...telementry.data, failed: true, 'failed.reason': 'user-cancelled' }, + source, + ); + + options?.generating?.cancel(); + return 'cancelled'; + } + + throw ex; + } if (cancellation.isCancellationRequested) { setLogScopeExit(scope, `model: ${model.provider.id}/${model.id}`, 'cancelled: user cancelled'); diff --git a/src/plus/ai/openAICompatibleProviderBase.ts b/src/plus/ai/openAICompatibleProviderBase.ts index 62679c7b4b5e6..da60092882c72 100644 --- a/src/plus/ai/openAICompatibleProviderBase.ts +++ b/src/plus/ai/openAICompatibleProviderBase.ts @@ -5,7 +5,7 @@ import { fetch } from '@env/fetch'; import type { Role } from '../../@types/vsls'; import type { AIProviders } from '../../constants.ai'; import type { Container } from '../../container'; -import { AIError, AIErrorReason, CancellationError } from '../../errors'; +import { AIError, AIErrorReason, CancellationError, isCancellationError } from '../../errors'; import { getLoggableName, Logger } from '../../system/logger'; import { startLogScope } from '../../system/logger.scope'; import type { ServerConnection } from '../gk/serverConnection'; @@ -38,7 +38,14 @@ export abstract class OpenAICompatibleProviderBase implem protected abstract readonly config: { keyUrl?: string; keyValidator?: RegExp }; async configured(silent: boolean): Promise { - return (await this.getApiKey(silent)) != null; + try { + const apiKey = await this.getApiKey(silent); + return apiKey != null; + } catch (ex) { + if (isCancellationError(ex)) return false; + + throw ex; + } } async getApiKey(silent: boolean): Promise { diff --git a/src/plus/ai/openRouterProvider.ts b/src/plus/ai/openRouterProvider.ts index f05c10a2e09bb..caabb29475e99 100644 --- a/src/plus/ai/openRouterProvider.ts +++ b/src/plus/ai/openRouterProvider.ts @@ -1,5 +1,6 @@ import { fetch } from '@env/fetch'; import { openRouterProviderDescriptor as provider } from '../../constants.ai'; +import { isCancellationError } from '../../errors'; import type { AIActionType, AIModel } from './models/model'; import { OpenAICompatibleProviderBase } from './openAICompatibleProviderBase'; @@ -15,7 +16,15 @@ export class OpenRouterProvider extends OpenAICompatibleProviderBase[]> { - const apiKey = await this.getApiKey(true); + let apiKey: string | undefined; + try { + apiKey = await this.getApiKey(true); + } catch (ex) { + if (isCancellationError(ex)) return []; + + throw ex; + } + if (!apiKey) return []; const url = 'https://openrouter.ai/api/v1/models'; diff --git a/src/plus/ai/utils/-webview/ai.utils.ts b/src/plus/ai/utils/-webview/ai.utils.ts index ff98866f89f96..ac91da0cc91fa 100644 --- a/src/plus/ai/utils/-webview/ai.utils.ts +++ b/src/plus/ai/utils/-webview/ai.utils.ts @@ -4,6 +4,7 @@ import { Schemes } from '../../../../constants'; import type { AIProviders } from '../../../../constants.ai'; import type { Container } from '../../../../container'; import type { MarkdownContentMetadata } from '../../../../documents/markdown'; +import { CancellationError } from '../../../../errors'; import { decodeGitLensRevisionUriAuthority } from '../../../../git/gitUri.authority'; import { createDirectiveQuickPickItem, Directive } from '../../../../quickpicks/items/directive'; import { configuration } from '../../../../system/-webview/configuration'; @@ -17,8 +18,8 @@ import { ensureAccountQuickPick } from '../../../gk/utils/-webview/acount.utils' import type { AIResult, AIResultContext } from '../../aiProviderService'; import type { AIActionType, AIModel } from '../../models/model'; -export function ensureAccount(container: Container, silent: boolean): Promise { - return ensureAccountQuickPick( +export async function ensureAccount(container: Container, silent: boolean): Promise { + const result = await ensureAccountQuickPick( container, createDirectiveQuickPickItem(Directive.Noop, undefined, { label: 'Use AI-powered GitLens features like Generate Commit Message, Explain Commit, and more', @@ -27,6 +28,12 @@ export function ensureAccount(container: Container, silent: boolean): Promise