From 44055224b5cdedc72b5d5f972068f58646b0a567 Mon Sep 17 00:00:00 2001 From: Cohen Date: Mon, 9 Mar 2026 13:14:17 -0700 Subject: [PATCH 1/2] aiEmbeddingVector: reject empty model during provider registration --- .../browser/mainThreadAiEmbeddingVector.ts | 5 ++ .../api/common/extHostEmbeddingVector.ts | 5 ++ .../common/aiEmbeddingVectorService.ts | 5 ++ .../common/aiEmbeddingVectorService.test.ts | 51 +++++++++++++++++++ 4 files changed, 66 insertions(+) create mode 100644 src/vs/workbench/services/aiEmbeddingVector/test/common/aiEmbeddingVectorService.test.ts diff --git a/src/vs/workbench/api/browser/mainThreadAiEmbeddingVector.ts b/src/vs/workbench/api/browser/mainThreadAiEmbeddingVector.ts index 68906b6ce3ab6..8f51275088fb4 100644 --- a/src/vs/workbench/api/browser/mainThreadAiEmbeddingVector.ts +++ b/src/vs/workbench/api/browser/mainThreadAiEmbeddingVector.ts @@ -8,6 +8,7 @@ import { Disposable, DisposableMap } from '../../../base/common/lifecycle.js'; import { ExtHostAiEmbeddingVectorShape, ExtHostContext, MainContext, MainThreadAiEmbeddingVectorShape } from '../common/extHost.protocol.js'; import { IAiEmbeddingVectorProvider, IAiEmbeddingVectorService } from '../../services/aiEmbeddingVector/common/aiEmbeddingVectorService.js'; import { IExtHostContext, extHostNamedCustomer } from '../../services/extensions/common/extHostCustomers.js'; +import { isFalsyOrWhitespace } from '../../../base/common/strings.js'; @extHostNamedCustomer(MainContext.MainThreadAiEmbeddingVector) export class MainThreadAiEmbeddingVector extends Disposable implements MainThreadAiEmbeddingVectorShape { @@ -23,6 +24,10 @@ export class MainThreadAiEmbeddingVector extends Disposable implements MainThrea } $registerAiEmbeddingVectorProvider(model: string, handle: number): void { + if (isFalsyOrWhitespace(model)) { + throw new Error('Embedding vector model must be a non-empty string.'); + } + const provider: IAiEmbeddingVectorProvider = { provideAiEmbeddingVector: (strings: string[], token: CancellationToken) => { return this._proxy.$provideAiEmbeddingVector( diff --git a/src/vs/workbench/api/common/extHostEmbeddingVector.ts b/src/vs/workbench/api/common/extHostEmbeddingVector.ts index 9f97d789b44bb..2884bbce84387 100644 --- a/src/vs/workbench/api/common/extHostEmbeddingVector.ts +++ b/src/vs/workbench/api/common/extHostEmbeddingVector.ts @@ -7,6 +7,7 @@ import { IExtensionDescription } from '../../../platform/extensions/common/exten import { ExtHostAiEmbeddingVectorShape, IMainContext, MainContext, MainThreadAiEmbeddingVectorShape } from './extHost.protocol.js'; import type { CancellationToken, EmbeddingVectorProvider } from 'vscode'; import { Disposable } from './extHostTypes.js'; +import { isFalsyOrWhitespace } from '../../../base/common/strings.js'; export class ExtHostAiEmbeddingVector implements ExtHostAiEmbeddingVectorShape { private _AiEmbeddingVectorProviders: Map = new Map(); @@ -38,6 +39,10 @@ export class ExtHostAiEmbeddingVector implements ExtHostAiEmbeddingVectorShape { } registerEmbeddingVectorProvider(extension: IExtensionDescription, model: string, provider: EmbeddingVectorProvider): Disposable { + if (isFalsyOrWhitespace(model)) { + throw new Error('Embedding vector model must be a non-empty string.'); + } + const handle = this._nextHandle; this._nextHandle++; this._AiEmbeddingVectorProviders.set(handle, provider); diff --git a/src/vs/workbench/services/aiEmbeddingVector/common/aiEmbeddingVectorService.ts b/src/vs/workbench/services/aiEmbeddingVector/common/aiEmbeddingVectorService.ts index e005d82cab6a0..a7da4cced9125 100644 --- a/src/vs/workbench/services/aiEmbeddingVector/common/aiEmbeddingVectorService.ts +++ b/src/vs/workbench/services/aiEmbeddingVector/common/aiEmbeddingVectorService.ts @@ -10,6 +10,7 @@ import { IDisposable } from '../../../../base/common/lifecycle.js'; import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js'; import { StopWatch } from '../../../../base/common/stopwatch.js'; import { ILogService } from '../../../../platform/log/common/log.js'; +import { isFalsyOrWhitespace } from '../../../../base/common/strings.js'; export const IAiEmbeddingVectorService = createDecorator('IAiEmbeddingVectorService'); @@ -40,6 +41,10 @@ export class AiEmbeddingVectorService implements IAiEmbeddingVectorService { } registerAiEmbeddingVectorProvider(model: string, provider: IAiEmbeddingVectorProvider): IDisposable { + if (isFalsyOrWhitespace(model)) { + throw new Error('Embedding vector model must be a non-empty string.'); + } + this._providers.push(provider); return { dispose: () => { diff --git a/src/vs/workbench/services/aiEmbeddingVector/test/common/aiEmbeddingVectorService.test.ts b/src/vs/workbench/services/aiEmbeddingVector/test/common/aiEmbeddingVectorService.test.ts new file mode 100644 index 0000000000000..8cf64b21ae4bb --- /dev/null +++ b/src/vs/workbench/services/aiEmbeddingVector/test/common/aiEmbeddingVectorService.test.ts @@ -0,0 +1,51 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import assert from 'assert'; +import { AiEmbeddingVectorService, IAiEmbeddingVectorProvider } from '../../common/aiEmbeddingVectorService.js'; +import { NullLogService } from '../../../../../platform/log/common/log.js'; +import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js'; + +suite('AiEmbeddingVectorService', () => { + const store = ensureNoDisposablesAreLeakedInTestSuite(); + let service: AiEmbeddingVectorService; + + setup(() => { + service = new AiEmbeddingVectorService(store.add(new NullLogService())); + }); + + test('should reject empty model when registering provider', () => { + const provider: IAiEmbeddingVectorProvider = { + provideAiEmbeddingVector: () => Promise.resolve([[1]]) + }; + + assert.throws( + () => service.registerAiEmbeddingVectorProvider('', provider), + /Embedding vector model must be a non-empty string\./ + ); + }); + + test('should reject whitespace-only model when registering provider', () => { + const provider: IAiEmbeddingVectorProvider = { + provideAiEmbeddingVector: () => Promise.resolve([[1]]) + }; + + assert.throws( + () => service.registerAiEmbeddingVectorProvider(' ', provider), + /Embedding vector model must be a non-empty string\./ + ); + }); + + test('should register and unregister provider with valid model', () => { + const provider: IAiEmbeddingVectorProvider = { + provideAiEmbeddingVector: () => Promise.resolve([[1]]) + }; + + const disposable = service.registerAiEmbeddingVectorProvider('test-model', provider); + assert.strictEqual(service.isEnabled(), true); + disposable.dispose(); + assert.strictEqual(service.isEnabled(), false); + }); +}); From e618e1c2d5da860a503650e8263568d38e9ded1b Mon Sep 17 00:00:00 2001 From: Cohen Date: Mon, 9 Mar 2026 13:44:31 -0700 Subject: [PATCH 2/2] aiEmbeddingVector: avoid throwing from main-thread RPC registration --- src/vs/workbench/api/browser/mainThreadAiEmbeddingVector.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/vs/workbench/api/browser/mainThreadAiEmbeddingVector.ts b/src/vs/workbench/api/browser/mainThreadAiEmbeddingVector.ts index 8f51275088fb4..68906b6ce3ab6 100644 --- a/src/vs/workbench/api/browser/mainThreadAiEmbeddingVector.ts +++ b/src/vs/workbench/api/browser/mainThreadAiEmbeddingVector.ts @@ -8,7 +8,6 @@ import { Disposable, DisposableMap } from '../../../base/common/lifecycle.js'; import { ExtHostAiEmbeddingVectorShape, ExtHostContext, MainContext, MainThreadAiEmbeddingVectorShape } from '../common/extHost.protocol.js'; import { IAiEmbeddingVectorProvider, IAiEmbeddingVectorService } from '../../services/aiEmbeddingVector/common/aiEmbeddingVectorService.js'; import { IExtHostContext, extHostNamedCustomer } from '../../services/extensions/common/extHostCustomers.js'; -import { isFalsyOrWhitespace } from '../../../base/common/strings.js'; @extHostNamedCustomer(MainContext.MainThreadAiEmbeddingVector) export class MainThreadAiEmbeddingVector extends Disposable implements MainThreadAiEmbeddingVectorShape { @@ -24,10 +23,6 @@ export class MainThreadAiEmbeddingVector extends Disposable implements MainThrea } $registerAiEmbeddingVectorProvider(model: string, handle: number): void { - if (isFalsyOrWhitespace(model)) { - throw new Error('Embedding vector model must be a non-empty string.'); - } - const provider: IAiEmbeddingVectorProvider = { provideAiEmbeddingVector: (strings: string[], token: CancellationToken) => { return this._proxy.$provideAiEmbeddingVector(